@qidiandasheng
2016-09-02T21:24:21.000000Z
字数 2647
阅读 2479
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 version
if (!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
进行消息转发。