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

来源:互联网 发布:京东商城与淘宝的区别 编辑:程序博客网 时间:2024/05/16 14:13

[编写高质量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, "v@:@");    }else{        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");    }    return YES;}// 实现getterid autoDictionaryGetter(id self, SEL _cmd){    EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;    NSMutableDictionary *backingStore = typedSelf.backingStore;    // getter的方法名就是属性的名称    NSString *key = NSStringFromSelector(_cmd);    return [backingStore objectForKey:key];}// 实现settervoid 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 STRING2016-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;}@endint 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:等类型信息查询方法,那么代理对象就会把这条消息转发给接受代理的对象,也就是说,这条消息的返回值与直接在接受代理的对象上查询其类型所得的结果相同。

4 0