[编写高质量iOS代码的52个有效方法](三)消息和运行期

[编写高质量iOS代码的52个有效方法](三)消息和运行期

参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway

先睹为快

11.理解objc_msgSend的作用

12.理解消息转发机制

13.用“方法调配技术”调试“黑盒方法”

14.理解“类对象”的用意

目录

  • 编写高质量iOS代码的52个有效方法三消息和运行期

    • 先睹为快
    • 目录
    • 第11条理解objc_msgSend的作用
    • 第12条理解消息转发机制
    • 第13条用方法调配技术调试黑盒方法
    • 第14条理解类对象的用意

第11条:理解objc_msgSend的作用

在对象上调用方法是Objective-C中经常使用的功能,用Objective-C的术语来说,叫做消息传递。消息有名称或选择器,可以接受参数,而且可能还有返回值。

由于Objective-C是C的超集,所以先以C语言为例来说明Objective-C中的消息传递机制:

#import <stdio.h>

void printHello(){
    print("Hello,world!\n");
}
void printGoodbye(){
    print("Goodbye,world!\n");
}

// 静态绑定方式
void staticBindingMethod(){
    if(type == 0){
        printHello();
    }
    else{
        printGoodbye();
    }
    return 0;
}

// 动态绑定方式
void dynamicBindingMethod(){
    void (*fnc)();
    if(type == 0){
        fnc = printHello;
    }
    else{
        fnc = printGoodbye;
    }
    fnc();
    return 0;
}

采用静态绑定,编译期就能决定运行时所应调用的函数,在本例中,if与else语句里都有函数调用指令。而采用动态绑定,所调用的函数直到运行期才能确定,在本例中,只有一个函数调用指令,且调用的函数地址不确定,需要在运行期读取出来。

在Objective-C中,如果向某对象传递消息,就会使用动态绑定机制来决定需要调用的方法。Objective-C是一门真正的动态语言。给对象发送消息可以这样来写:

id returnValue = [someObject messageName:parameter];

在本例中,someObject叫做接收器,messageName叫做选择器,选择器和参数合起来称为消息。编译器看到此消息后将其转换为一条标准的C语言函数调用:

// objc_msgSend函数原型,该函数是消息传递机制中的核心函数
void objc_msgSend(id self, SEL cmd, ...);

// 编译器将上面例子中的消息转换为如下函数
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

还有一些特殊情况,交由另一些类似objc_msgSend的函数处理:

objc_msgSend_stret  // 待发送的消息返回的是结构体
objc_msgSend_fpret  // 待发送的消息返回的是浮点数
objc_msgSendSuper   // 向超类发送消息,也有等效于上述两个函数的函数用于处理发送给超类的相应消息

如果某函数的最后一项操作是调用另一个函数,那么就可以运用尾调用优化技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的栈帧。只有当某函数的最后一个操作仅仅只是调用其他函数而不会将其返回值另做他用时,才能执行尾调用优化。这项优化非常关键,可以避免过早地发生栈溢出现象。

第12条:理解消息转发机制

由于Objective-C的动态特性,在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译期在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动消息转发机制。

消息转发分为两大阶段,第一阶段叫做动态方法解析,即征询接收者,所属的类,看其是否能动态添加方法以处理当前这个未知的选择器。第二阶段,运行期系统会请求接收者将消息转发给其他对象来处理,又分两小步,第一小步为快速转发,如果快速转发也不能处理,则进行第二小步标准转发(完整转发)。

// 动态方法解析调用的实例方法和类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

// 快速转发调用的方法
- (id)forwardingTargetForSelector:(SEL)aSelector;

// 标准转发调用的方法
- (void)forwardInvocation:(NSInvocation *)anInvocation;

接收者在每一步中均有机会处理消息,但步骤越往后,处理消息的代价就越大。

下面是一个动态方法解析的示例,以动态的方式实现类似getter和setter的方法:

//EOCAutoDictionary.h
#import <Foundation/Foundation.h>

@interface EOCAutoDictionary : NSObject

@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;

@end

// EOCAutoDictionary.m
#import "EOCAutoDictionary.h"
#import <objc/runtime.h>

@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end

@implementation EOCAutoDictionary

// 将属性声明为@dynamic,则编译器不会为其自动生成实例变量及存取方法
@dynamic string, number, date, opaqueObject;

-(id)init{
    if ((self = [super init])) {
        _backingStore = [NSMutableDictionary new];
    }
    return self;
}

// 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selectorString = NSStringFromSelector(sel);
    // 根据选择器名是否以set开头选择动态添加的方法
    if ([selectorString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictionarySetter, "[email protected]:@");
    }else{
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return YES;
}

// 实现getter
id autoDictionaryGetter(id self, SEL _cmd){
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;

    // getter的方法名就是属性的名称
    NSString *key = NSStringFromSelector(_cmd);

    return [backingStore objectForKey:key];
}

// 实现setter
void autoDictionarySetter(id self, SEL _cmd, id value){
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;

    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString copy];

    // setter的方法名转换成属性名,需要去掉末尾的冒号、开头的set,并将属性名的开头的大写字母转为小写
    // 如 date 属性的setter方法名为 setDate:
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];

    if (value) {
        [backingStore setObject:value forKey:key];
    }else{
        [backingStore removeObjectForKey:key];
    }
}
@end

快速转发和标准转发的实现可以分别参考:

快速转发示例

标准转发示例

第13条:用“方法调配技术”调试“黑盒方法

类的方法列表会把选择器的名称映射到相关的方法实现之上,使得动态消息派发系统能够据此找到应调用的方法。Objective-C运行期系统提供了几个方法可以操作选择器的映射表,可以新增选择器,改变选择器对应的方法实现,或者交换两个选择器所映射的指针。这就是方法调配。

下面是用方法调配技术来交换NSString中的大小写转换方法:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        // 从类中获取两个方法,再将其进行交换
        Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
        Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
        method_exchangeImplementations(originalMethod, swappedMethod);

        NSString *string = @"ThIs iS tHe StRiNg";

        NSString *lowercaseString = [string lowercaseString];
        NSLog(@"lowercaseString = %@", lowercaseString);

        NSString *uppercaseString = [string uppercaseString];
        NSLog(@"uppercaseString = %@", uppercaseString);
    }
    return 0;
}

运行结果:

2016-07-25 14:21:25.058 OCTest[31026:1747037] lowercaseString = THIS IS THE STRING
2016-07-25 14:21:25.059 OCTest[31026:1747037] uppercaseString = this is the string

可以用这个方法来为完全不透明的黑盒方法增加日志记录功能,这非常有助于程序调试。下面就为lowercaseString方法添加日志记录功能:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

// 在NSString分类中添加并实现一个新方法
@interface NSString(EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end

@implementation NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString{
    // 看似会陷入死循环,但运行期进行调换后,实际调用的是lowercaseString方法
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ => %@", self, lowercase);
    return lowercase;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 调换方法
        Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
        Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
        method_exchangeImplementations(originalMethod, swappedMethod);

        NSString *string = @"ThIs iS tHe StRiNg";
        // 现在调用lowercaseString方法会输出日志
        [string lowercaseString];
    }
    return 0;
}

运行结果:

2016-07-25 14:30:29.847 OCTest[31568:1753042] ThIs iS tHe StRiNg => this is the string

注意:只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

第14条:理解“类对象”的用意

对象类型并非在编译期就绑定好了,而是要在运行期查找。在运行期检视对象类型这一操作也叫做类型信息查询,这个强大而有用的特性内置于NSObject协议中,凡是有公共根类(NSObject与NSProxy)继承而来的对象都要遵从此协议。

isMemberOfClass:能够判断出对象是否为某个特定类的实例,而isKindOfClass:能够判断出对象是否为某类或其派生类的实例,例如:

NSMutableDictionary *dict = [NSMutableDictionary new];

[dict isMemberOfClass:[NSDictionary class]]; // NO
[dict isMemberOfClass:[NSMutableDictionary class]]; // YES
[dict isKindOfClass:[NSDictionary class]]; // YES
[dict isKindOfClass:[NSArray class]]; // NO

下面是用类型信息查询根据数组中存储的对象生成以逗号分隔的字符串:

-(NSString*)commaSeparatedStringFromObjects:(NSArray*)array{
    NSMutableString *string = [NSMutableString new];
    for (id object in array) {
        if ([object isKindOfClass:[NSString class]]) {
            [string appendFormat:@"%@,", object];
        }else if ([object isKindOfClass:[NSNumber class]]){
            [string appendFormat:@"%d,", [object intValue]];
        }else if ([object isKindOfClass:[NSData class]]){
            NSString *base64Encoded = /* base64 encodes data */
            [string appendFormat:@"%@,",base64Encoded];
        }else{
            // 处理不支持类型
        }
    }
    return string;
}

比较类对象是否等同是,不要使用“isEqual:”方法,因为类对象是单例,在应用程序范围内,每个类的Class仅有一个实例,另一种精确判断对象类型的办法是使用“==”:

id object = /* ... */
if ([object class] == [EOCSomeClass class]){
    // code
}

但更好的方法仍旧是使用类型信息查询方法,因为它可以正确处理那些使用了消息传递机制的对象。如果在代理对象上调用class方法,那么返回的是代理对象本身(NSProxy的子类),而非接受代理的对象所属的类。若是改用isKindOfClass:等类型信息查询方法,那么代理对象就会把这条消息转发给接受代理的对象,也就是说,这条消息的返回值与直接在接受代理的对象上查询其类型所得的结果相同。

时间: 2024-08-04 22:20:01

[编写高质量iOS代码的52个有效方法](三)消息和运行期的相关文章

[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)

[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD) 参考书籍:<Effective Objective-C 2.0> [英] Matt Galloway 先睹为快 41.多用派发队列,少用同步锁 42.多用GCD,少用performSelector系列方法 43.掌握GCD及操作队列的使用时机 44.通过Dispatch Group机制,根据系统资源状况来执行任务 45.使用dispatch_once来执行只需要运行一次的线程安全代码 46.不

[编写高质量iOS代码的52个有效方法](十一)系统框架

[编写高质量iOS代码的52个有效方法](十一)系统框架 参考书籍:<Effective Objective-C 2.0> [英] Matt Galloway 先睹为快 47.熟悉系统框架 48.多用块枚举,少用for循环 49.对自定义其内存管理语义的容器使用无缝桥接 50.构建缓存时选用NSCache而非NSDictionary 51.精简initialize与load的实现代码 52.别忘了NSTimer会保留其目标对象 目录 编写高质量iOS代码的52个有效方法十一系统框架 先睹为快

[编写高质量iOS代码的52个有效方法](一)Objective-C基础

[编写高质量iOS代码的52个有效方法](一)Objective-C基础 参考书籍:<Effective Objective-C 2.0> [英] Matt Galloway 先睹为快 1.了解Objective-C语言的起源 2.在类的头文件中尽量少引入其他头文件 3.多用字面量语法,少用与之等价的方法 4.多用类型常量,少用#define预处理器指令 5.用枚举表示状态.选项.状态码 目录 编写高质量iOS代码的52个有效方法一Objective-C基础 先睹为快 目录 第1条了解Obje

【iOS】编写高质量iOS代码的52个有效方法

<编写高质量iOS与OS X代码的52个有效方法>的笔记,通读了一遍,感觉印象不是特别深刻,写下笔记记录下吧. 这个数的结构很清晰,5章52条建议,分点介绍了编写高效代码的建议,每点后面有总结,这里简单的记录下这些总结,方便温习. 因时间原因,先写5条,有时间逐渐加进来. 第一章:熟悉oc 1,了解oc起源 oc为C语言添加了面向对象特性,是其超集.oc使用动态绑定的消息结构,也就是说已在运行时才会检查对象类型.接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定. 理解C语言的

编写高质量JavaScript代码的68个有效方法

简介: <Effective JavaScript:编写高质量JavaScript代码的68个有效方法>共分为7章,分别涵盖JavaScript的不同主题.第1章主要讲述最基本的主题,如版本.类型转换要点.运算符注意事项和分号局限等.第2章主要讲解变量作用域,介绍此方面的一些基本概念,以及一些最佳实践经验.第3章主要讲解函数的使用,深刻解析函数.方法和类,并教会读者在不同的环境下高效使用函数.第4章主要讲解原型和对象,分析JavaScript的继承机制以及原型和对象使用的最佳实践和原则.第5章

Effective Python之编写高质量Python代码的59个有效方法

                                                     这个周末断断续续的阅读完了<Effective Python之编写高质量Python代码的59个有效方法>,感觉还不错,具有很大的指导价值.下面将以最简单的方式记录这59条建议,并在大部分建议后面加上了说明和示例,文章篇幅大,请您提前备好瓜子和啤酒! 1. 用Pythonic方式思考 第一条:确认自己使用的Python版本 (1)有两个版本的python处于活跃状态,python2和pyt

编写高质量Python代码的59个有效方法

作者Brett Slatkin是 Google公司高级软件工程师.他是Google消费者调查项目的工程主管及联合创始人,曾从事Google App Engine的Python基础架构工作,并利用Python来管理众多的Google服务器.Slatkin也是PubSubHubbub协议的联合创始人,还用Python为Google实现了针对该协议的系统.他拥有哥伦比亚大学计算机工程专业学士学位. 精彩书评 "Slatkin所写的这本书,其每个条目(item)都是一项独立的教程,并包含它自己的源代码.

JavaScript手札:《编写高质量JS代码的68个有效方法》(一)(1~5)

编写高质量JS代码的68个有效方法(一) *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* BLOCKS =============================================================================*/ p, blockquote, ul, ol, dl, table, pre { marg

编写高质量JS代码的68个有效方法(三)

[20141030]编写高质量JS代码的68个有效方法(三) *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* BLOCKS =============================================================================*/ p, blockquote, ul, ol, dl, table,