@qidiandasheng
2016-09-02T13:24:21.000000Z
字数 2647
阅读 2821
iOS理论
说到消息就要分两块:一块是消息发送,还有一块就是消息转发。
消息发送就是Runtime 通过 selector 快速查找 IMP(函数指针) 的过程。
消息转发就是在消息发送中找不到IMP然后进入一系列转发流程的过程。
说道消息我们肯定需要说一下方法,方法在runtime中的结构体为objc_method,具体定义在runtime源码runtime.h文件中,如下:
struct objc_method {//方法名SEL method_name OBJC2_UNAVAILABLE;//方法类型,描述了参数的类型char *method_types OBJC2_UNAVAILABLE;//函数指针,为方法具体实现代码块的地址IMP method_imp OBJC2_UNAVAILABLE;}
可以通过- (IMP)methodForSelector:(SEL)aSelector;获取实例方法的函数指针。
通过+ (IMP)instanceMethodForSelector:(SEL)aSelector获取类方法的函数指针。
这里我们能通过获得IMP直接调用方法:
- (void)viewDidLoad {[super viewDidLoad];IMP imp = [self methodForSelector:@selector(printHellWorld)];imp();}- (void)printHellWorld{NSLog(@"Hell World");}
打印结果:
2016-09-02 19:53:03.250 test[46586:7508586] Hell World
消息的发送是通过objc_msgSend来的,他的伪代码如下所示:
id objc_msgSend(id self, SEL _cmd, ...) {Class class = object_getClass(self);IMP imp = class_getMethodImplementation(class, _cmd);return imp ? imp(self, _cmd, ...) : 0;}
我们可以看到他通过class(类)和_cmd(方法子)来获取到这个函数指针,然后执行对应的函数。
我们在分析class的结构objc_class是看到有两个属性:struct objc_method_list **methodLists和struct objc_cache *cache。
在查找IMP的过程中,首先会根据SEL作为key去类的cache方法缓存中查找,如果找到直接返回IMP,如果找不到就去methodLists方法列表中查找,如果找到返回IMP(同时把方法加入到cache中,方便下次快速查找)。
如果找不到就去父类中找循环以上的过程,直到到最顶层的NSObjec中都找不到IMP了,就会开始准备进入方法转发的过程。
消息转发可以间接实现多继承。
我们看到上面objc_method的伪代码中有这么一段class_getMethodImplementation函数来得到IMP,我们来看看这个函数的定义(在objc-class.mm文件中):
IMP class_getMethodImplementation(Class cls, SEL sel){IMP imp;if (!cls || !sel) return nil;imp = lookUpImpOrNil(cls, sel, nil,YES/*initialize*/, YES/*cache*/, YES/*resolver*/);// Translate forwarding function to C-callable external versionif (!imp) {return _objc_msgForward;}return imp;}
我们能看到当找不到imp的时候此函数返回了一个_objc_msgForward。其实_objc_msgForward也是函数指针,只是他是在类及父类中找不到对应的方法时才返回,主要就是用于消息转发。
如果直接使用_objc_msgForward相当于就是跳过查找类中的IMP的过程,直接进行消息转发。就算这个类中有这个方法,也不会执行了,这样的话会很危险。
我们可以先看看_objc_msgForward是如何进行转发的。
我们先来发送一个错误的消息,来看看整个消息转发是如何进行的。
call (void)instrumentObjcMessageSends(YES)

然后继续运行程序,运行时发送的所有消息都会打印到/tmp/msgSend-xxxx文件里了。
直接打开/private/tmp目录找到最新的msgSend-xxxx文件。我们看到文件最上面如下所示:

也就是说消息转发主要就是做了以下几件事:
1:调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。
2:调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。
3:调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
4:调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。
5:调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
具体过程如下图所示:

JSPatch中用到了_objc_msgForward进行消息转发。