写在前面
前段时间写了一篇博客runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法),这是在看《招聘一个靠谱的iOS》时回答第22题时总结的一篇博客,不过这篇博客中并没有牵涉到底层的代码,而且也留下了几个没有解决的问题,这篇博客将深入runtime源码继续探索这个问题,并尝试解决上篇博客中未解决的问题,本人第一次阅读源码,如果有分析错误的地方,欢迎大家纠正。
引入
首先大家都知道,在oc中调用方法(或者说发送一个消息是)runtime底层都会翻译成objc_msgSend(id self, SEL op, ...)
,苹果为了优化性能,这个方法是用汇编写成的
|
|
实话说我没有学过汇编,所以看到这段代码我的内心是崩溃的,更可怕的是针对不同的平台,还有不同汇编代码的实现

虽然不懂汇编,但是苹果的注释很详细,看注释也可以大致明白在干什么,首先检查传入的self是否为空,然后根据selector寻找方法实现IMP,找到则调用并返回,否则抛出异常。由此可以有以下伪代码
|
|
伪代码中我们看到class_getMethodImplementation(Class cls, SEL sel)
方法用来寻找IMP地址,有趣的是苹果真的提供了这个方法,可以让我们调用,通过selector去寻找方法实现IMP,而这个函数的实现,以及其延伸就是这篇博客所要探讨的重点。
正文
在我前面的文章中也说到IMP寻址总共有两种方法:
|
|
而在NSObject中提供了几个对class_getMethodImplementation封装的方法
|
|
但这些方法却并没有在头文件中暴露,所以我并不明白苹果这样做的用意,如果有人知道,希望能够告知,感激不尽!
这里出现的object_getMethodImplementation其实就是对class_getMethodImplementation的封装,苹果的解释是:
Equivalent to: class_getMethodImplementation(object_getClass(obj), name);
下面我们就暂时把目光转向class_getMethodImplementation这个函数,看看它底层到底是如何实现的
|
|
首先判断传入的参数是否为空,然后进入lookUpImpOrNil这个方法,实际上这个这个方法是对lookUpImpOrForward的简单封装:
|
|
注释写的也很清楚,这个方法不会进行消息的转发,而直接返回nil,这个倒是比较有趣,明明调用lookUpImpOrForward可以直接进行消息转发,可是这里偏不这样做,调用消息转发返回nil的函数,然后判断imp为nil时,自己手动返回_objc_msgForward,进行消息转发,还真是有意思,不过苹果在这里做了注释:Translate forwarding function to C-callable external version,将这个转发函数转换为C语言能够调用的版本。
接下来我们继续深入,看一下lookUpImpOrForward是如何实现的:
|
|
我天,好长的一段代码,我删了好多注释,还是很多
首先这里有一个我并不懂的东西methodListLock.assertUnlocked();
我看到Objective-C 消息发送与转发机制原理 中对此的解释是
对 debug 模式下的 assert 进行 unlock,runtimeLock 本质上是对 Darwin 提供的线程读写锁 pthread_rwlock_t 的一层封装,提供了一些便捷的方法。
需要注意的是在objc-runtime-new.mm中有一段几乎相同的lookUpImpOrForward的实现,在该实现中,加锁操作是runtimeLock.read();
所以这篇上述博客使用的代码应该是objc-runtime-new.mm的代码,而我的源码是来自于objc-class-old.mm 虽然名称不同,但我想底层应该是一样的。
很佩服这位博主对底层认识的如此深刻,我们暂时就按照这里写的理解,继续往下看
无锁的缓存查找(Optimistic cache lookup)
在没有锁的状态下进行缓存搜索,性能会比较好
首先如果cache传入的是YES,则调用cache_getImp
在缓存中搜索,当然这里传入的是YES(而在objc_msgSend方法里在这里进行了优化,objc_msgSend最开始就在缓存中进行了搜索,所以有了一个很有趣的方法_class_lookupMethodAndLoadCache3
,这个方法在调用lookUpImpOrForward时传入cache是NO,避免两次搜索缓存),而cache_getImp
是用汇编写的(又是汇编。。。(T_T))
|
|
具体的缓存搜索是在宏CacheLookup
中实现的,具体这里就不展开了(也展开不了,我还没看懂(^-^) )。
释放检测
|
|
检测发送消息的对象是否已经被释放,如果已经释放,则返回_freedHandler
的IMP
|
|
在该方法中抛出message sent to freed object的错误信息(不过我还从来没有遇到过这样的错误信息)
初始化检查
|
|
这里我不是很理解+initialize方法是做什么的
|
|
但是从isInitialized()
的实现来看初始化的信息保存在元类中,由此推测是元类或者是类对象的初始化工作,而我在上文中提到的博客中是这样写的:
如果是第一次用到这个类且 initialize 参数为 YES(initialize && !cls->isInitialized()),需要进行初始化工作,也就是开辟一个用于读写数据的空间。先对 runtimeLock 写操作加锁,然后调用 cls 的 initialize 方法。如果 sel == initialize 也没关系,虽然 initialize 还会被调用一次,但不会起作用啦,因为 cls->isInitialized() 已经是 YES 啦。
这里的表述也大致印证了我的猜测,是对类对象或者是元类对象进行初始化的工作,不过我还是有一点不明白:类对象都还没有初始化,那是如何产生这个类的实例对象呢?然而在别人博客中看到:+load是在runtime之前就被调用的,+initialize是在runtime才调用
retry语句标号(在该类的父类中查找)
这里对方法列表进行了加锁的操作methodListLock.lock();
The lock is held to make method-lookup + cache-fill atomic with respect to method addition. Otherwise, a category could be added but ignored indefinitely because the cache was re-filled with the old value after the cache flush on behalf of the category.
考虑运行时方法的动态添加,加锁是为了使方法搜索和缓存填充成为原子操作。否则category添加时刷新的缓存可能会因为旧数据的重新填充而被完全忽略掉。
|
|
|
|
- 检查selector是否是垃圾回收方法,如果是则填充缓存
_cache_fill(cls, (Method)entryp, sel);
(这里entryp的类型是结构体cache_entry,将其强转为Method,我们可以看到上面的代码,OC2.0前,这个cache_entry和method的定义几乎是相同的,2.0后加入了一个我完全看不懂的东西(T_T))并让methodPC指向该方法的实现即entryp->imp(实际上这是一个汇编程序的入口_objc_ignored_method
),然后跳转到done语句标号。否则进行下一步 - 在本类的缓存中查找,也是使用汇编程序入口
_cache_getImp
,如果找到,跳转到done语句标号,否则进行下一步。 - 在上一步缓存中没有发现,然后进入本类的方法列表中查找,如果找到了则进行缓存填充,并让methodPC指向该方法的实现,跳转到done语句标号,否则进行下一步。
- 在父类的方法列表和缓存中递归查找,首先是查找缓存,又是调用一个汇编的程序入口
_cache_getMethod
比较奇怪的是我只在objc-msg-i386.s中发现了这个程序入口,与前面不同的是,这里传入了一个_objc_msgForward_impcache
的汇编程序入口作为缓存中消息转发的标记,如果发现缓存的方法,则使method_PC指向其实现,跳转到done语句标号,如果找到了Method,但发现其IMP是一个转发的汇编程序入口即_objc_msgForward_impcache
,立即跳出循环,但是不立刻缓存,而是call method resolver,即进行第5步。如果缓存中没发现Method,就在列表中寻找,同样是找到即跳转到done,否则进行下一步。 - 当传入的resolver为YES且triedResolver为NO时(即此步骤只会进入一次,进入后triedResolver会设为YES),进入method resolver(动态方法解析),首先对methodListLock解锁,然后调用
_class_resolveMethod
发送_class_resolveInstanceMethod
或_class_resolveClassMethod
消息,程序员此时可以动态的给selector添加一个对应的IMP。完成后再回到第1步重新来一遍。这一步消息转发前最后一次机会。 - 没有找到方法的实现,method resolver(动态方法解析)也没有作用,此时进行消息的转发,使methodPC指向
_objc_msgForward_impcache
汇编程序入口,并进入done。done语句标号
首先将methodListLock解锁,然后断言不会存在一个被忽略的selector其implementation是没有被忽略的(官方的意思是非要找到这样一个selector,真是有趣)paranoia: look for ignored selectors with non-ignored implementations
最后返回这个methodPC。
然后就是消息转发部分了,其objc_setForwardHandler实现机制不在Objective-C Runtime (libobjc.dylib)中,而是在CoreFoundation(CoreFoundation.framework)中,所以这里就先不讨论了,等我以后研究了那部分以后,再专门写一篇关于消息转发的博客。
关于正文开始处所说的第二种方法method_getImplementation(),首先需要调用class_getInstanceMethod()
而在这个方法里加了一个warning
|
|
我这里调用了lookUpImpOrNil方法,却没有使用其返回值,而且标注需要fix and search caches,我猜测可能因为某种原因,在这里无法进行缓存查找,而后面return _class_getMethod(cls, sel);
本质上就是在方法列表中进行查找,而且也没有进行消息转发。
这里也印证了苹果对于这个方法的注释:class_getMethodImplementation may be faster than method_getImplementation(class_getInstanceMethod(cls, name)),因为第一个方法进行了缓存的查找,如果缓存中能找到,效率会提高很多。
以前的问题
在我上一篇博客runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)里我有一个没有解决的问题:为什么对于无法找到的IMP,class_getMethodImplementation(),method_getImplementation()返回值会不一样?
|
|
看完源码,就很清楚了,如果这个method不存在,直接返回nil,而
class_getMethodImplementation()会经历消息转发机制,最后返回的是forwardInvocation的结果,而这部分是不开源的,也不知道具体是怎么返回的,但每次运行确实是会返回的一个固定的地址,我猜测最后这个地址可能和NSInvocation这个对象的内存地址有关,具体那是什么地址,以后有机会在去寻找答案。
结语
如果我上面的分析或推测有错误,欢迎指正,大家一同成长,我在写这篇博客时参考的博客有:Objective-C 消息发送与转发机制原理、Objective-C 源码(二)+load 以及 +initialize这里将其贴出,感谢这些博客的作者,跟这些博客相比,我的博客写的真的很菜,毕竟刚开始,相信有一天我也能写出如此优秀的博客。
这篇博客中使用的runtime源码版本是objc4-680。