消息机制
Objective-C是运行时语言,消息机制是运行时机制中一个重要的组成部分。方法调用的本质就是发送消息。
实现方式
定义一个Person类,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @interface Person : NSObject
@property (nonatomic, assign)NSUInteger age;
- (NSString *)sayHello;
- (NSString *)howOldAreYou:(NSString *)name;
+ (NSString *)howOldAreYou:(NSString *)name;
@end
@implementation Person
- (NSString *)sayHello { return @"Thank you very much!"; }
- (NSString *)howOldAreYou:(NSString *)name { return [NSString stringWithFormat:@"%@ 18 years old!", name]; }
+ (NSString *)howOldAreYou:(NSString *)name { return [[[self alloc] init] howOldAreYou:name]; }
@end
|
向Person实例发送howOldAreYou消息的方式,有如下几种:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| - (void)demo { Person *person = [[Person alloc] init]; // 1.直接调用 NSString *rst1 = [person howOldAreYou:@"kobe"]; NSLog(@"直接调用 -> %@", rst1); // 2.performSelector NSString *rst2 = [person performSelector:@selector(howOldAreYou:) withObject:@"kobe"]; NSLog(@"performSelector -> %@", rst2); // 3.方法签名 + NSInvocation NSMethodSignature *signature = [person methodSignatureForSelector:@selector(howOldAreYou:)]; // 获取方法签名对应的invocation NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; /** 设置消息接受者,与[invocation setArgument:(__bridge void * _Nonnull)(person) atIndex:0]等价 */ [invocation setTarget:person]; /**设置要执行的selector,与[invocation setArgument:@selector(howOldAreYou:) atIndex:1] 等价*/ [invocation setSelector:@selector(howOldAreYou:)]; // 设置参数 NSString *str = @"kobe"; [invocation setArgument:&str atIndex:2]; // 开始执行 [invocation invoke]; void *returnValue = NULL; if (signature.methodReturnLength) { [invocation getReturnValue:&returnValue]; } id rst3 = (__bridge id)returnValue; NSLog(@"方法签名 -> %@", rst3); // 4.objc_msgSend id rst4 = ((id(*)(id, SEL, NSString *))objc_msgSend)(person, @selector(howOldAreYou:), @"kobe"); NSLog(@"objc_msgSend -> %@", rst4); }
|
该代码的关键之处主要有以下几点:
* import runtime相关的头文件<objc/message.h>
。
* 使用objc_msgSend
函数来负责消息发送。
向Person类发送howOldAreYou消息的方式,有如下几种:
1 2 3 4 5 6 7 8 9 10 11 12
| - (void)demo { Class clazz = [Person class]; // 1.直接调用 NSString *rst1 = [Person howOldAreYou:@"Mary"]; NSLog(@"直接调用 -> %@", rst1); // 2.performSelector NSString *rst2 = [clazz performSelector:@selector(howOldAreYou:) withObject:@"Mary"]; NSLog(@"performSelector -> %@", rst2); // 3.objc_msgSend id rst3 = ((id(*)(id, SEL, NSString *))objc_msgSend)(clazz, @selector(howOldAreYou:), @"Mary"); NSLog(@"objc_msgSend -> %@", rst3); }
|
注意:使用objc_msgSend
函数时,第一步需要#import <objc/message.h>
。第二步需要关闭objc_msgSend函数的严格check(Build Setting -> 搜索msg -> 设置属性为No)。如果不关闭,就需要手动对objc_msgSend
函数进行类型转换。
分析:[person howOldAreYou:@"kobe"]
,它的含义是:向person发送名为howOldAreYou:
的消息。通过clang -rewrite-objc ViewController.m
指令,将该句重写为C代码:((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("howOldAreYou:"));
可知底层是基于objc_msgSend的,去掉那些强制转换,最终[person howOldAreYou:@"kobe"]
会由编译器转化为以下的纯C调用。
objc_msgSend(person, @selector(howOldAreYou:), @"kobe");
,所以说,objc发送消息,最终大都会转换为objc_msgSend
的方法调用。
消息发送的主要步骤:
OC的方法调用最终会生成C函数objc_msgSend(person, @selector(howOldAreYou:), @"kobe")。这个C函数objc_msgSend就负责消息发送。
消息发送的时候,在C语言函数中发生了什么事情?编译器是如何找到这个方法的呢?消息发送的主要步骤如下:
1.首先检查这个选择器是不是要忽略。比如Mac OSX开发,有了垃圾回收就不会理会retain,release这些函数。
2.检测这个选择器的target是不是nil,OC允许我们对一个nil对象执行任何方法不会Crash,因为运行时会被忽略掉。
3.如果上面两步都通过了,根据实例的isa指针找到类对象,开始查找这个SEL的实现IMP,先从类对象的cache里查找,如果找到了就运行对应的函数去执行相应的代码。
4.如果cache中没有找到就找类的methodLists中是否有对应的方法。
5.如果类的方法列表中找不到就到父类的方法列表中查找,一直找到NSObject类为止。
6.如果还是没找到就要开始进入动态方法解析和消息转发,后面会说。
应用场景
1.字典转模型
原理:遍历模型类的所有属性,然后用属性名作为key去字典中找对应的value,如果找不到不会crash。
关键代码:
2.KVO
原理:系统自动新建一个被监听类的子类,重写被监听属性的set方法,修改被监听对象的isa指针的值,让它指向子类对象。
关键代码:
使用objc_setAssociatedObject和objc_getAssociatedObject关联对象。
使用object_setClass函数修改对象isa指针。
消息转发
触发机制
在正常消息流程走完,没有找到SEL对应的IMP的时候,就会进入消息转发机制。
定义一个示例类:ForwardObject
1 2 3 4
| @interface ForwardObject : NSObject @end @implementation ForwardObject @end
|
调用ForwardObject中不存在的test
实例方法。
1 2 3 4 5 6
| - (void)viewDidLoad { [super viewDidLoad]; ForwardObject *obj = [[ForwardObject alloc] init]; [obj performSelector:@selector(test)]; }
|
运行之后,系统报错'-[ForwardObject test]: unrecognized selector sent to instance 0x600002a9c800'
类似的报错都是iOS的消息转发机制在无法响应方法之后抛出的错误。
实现原理
当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会尝试做消息转发,具体动作如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // 允许用户在此时为该Class动态添加实现。 // 如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。 + (BOOL)resolveInstanceMethod:(SEL)sel;
// 尝试找到一个能响应该消息的对象。 // 如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。 - (id)forwardingTargetForSelector:(SEL)aSelector;
// 尝试获得一个方法签名。 // 如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 将获取到的方法签名包装成Invocation传入,如何处理就在这里面了。 - (void)forwardInvocation:(NSInvocation *)anInvocation;
|
上面这4个方法均是模板方法,开发者可以重写,由runtime来调用。以下是最常见的实现消息转发方式:
1.重写resolveInstanceMethod:
方法,在此时为该Class动态添加实现。(动态方法解析)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| + (BOOL)resolveInstanceMethod:(SEL)sel { if ([NSStringFromSelector(sel) isEqualToString:@"test"]) { Method method = class_getInstanceMethod(self.class, @selector(dynamicAddMethod)); IMP methodIMP = method_getImplementation(method); const char *methodType = method_getTypeEncoding(method); class_addMethod(self.class, sel, methodIMP, methodType); return YES; } return [super resolveInstanceMethod:sel]; }
- (void)dynamicAddMethod { NSLog(@"动态添加的test方法"); }
|
log信息为:动态添加的test方法
,由此可知,通过重写resolveInstanceMethod:
方法,给Class动态添加test
实现,完成了消息转发机制。
2,重写forwardingTargetForSelector:
方法,尝试找到一个能响应该消息的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @interface BackupForwardObject : NSObject - (void)test; @end @implementation BackupForwardObject - (void)test { NSLog(@"备用类的test方法"); } @end
- (id)forwardingTargetForSelector:(SEL)aSelector { if ([NSStringFromSelector(aSelector) isEqualToString:@"test"]) { return [[BackupForwardObject alloc] init]; } return [super forwardingTargetForSelector:aSelector]; }
|
log信息为:备用类的test方法
,由此可知,通过重写forwardingTargetForSelector:
方法,找到一个能响应该消息的对象,完成消息转发机制。
3.重写methodSignatureForSelector:
方法和forwardInvocation:
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (![super methodSignatureForSelector:aSelector]) { NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"]; return methodSignature; } return [super methodSignatureForSelector:aSelector]; }
- (void)forwardInvocation:(NSInvocation *)anInvocation { BackupForwardObject *backUp = [[BackupForwardObject alloc] init]; SEL sel = anInvocation.selector; // 判断备用对象是否可以响应传递进来等待响应的SEL if ([backUp respondsToSelector:sel]) { [anInvocation invokeWithTarget:backUp]; } else { [super forwardInvocation:anInvocation]; } }
|
log信息为:备用类的test方法
,由此可知,吞掉一个消息或者代理给其他对象都是没问题的。
方式3跟方式2本质上是一样的都是变更接受消息的对象,但是方式3变更响应目标更复杂一些,方式2只需返回一个可以响应的对象即可,方式3还需要手动将响应方法切换给备用响应对象。
以上三种方式都可以实现消息转发,但是越往后面处理代价越高,最好的情况是在第一步就处理消息,这样runtime会在处理完后缓存结果,下回再发送同样消息的时候,可以提高处理效率。方式2转移消息的接受者也比进入转发流程的代价要小,如果到最后一步forwardInvocation的话,就需要处理完整的NSInvocation对象了。
应用场景
1.JSPatch –iOS动态化更新方案
Demo1
Demo2
2.实现多重代理
Demo