「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。
在前一篇底层探索 -- OC消息机制(一)objc_msgSend快速查找中已经完成了对IMP快速查找流程的分析,本篇则接着上一篇的篇尾,继续分析
objc_msgSend_uncached慢速查找流程。(本篇总字约2071字)
本篇重点
lookUpImpOrForward慢速查找流程及其内部细节分析。
一、objc_msgSend_uncached分析
快速查找中CacheHit的逻辑已经分析过了,接下来分析没找到时会进入的__objc_msgSend_uncached 方法,看看其内部的对于查找是如何处理的:
objc_msgSend_uncached主要的逻辑内容在MethodTableLooup中,看其名称也能猜到:准备去找Cls->method_list了.- 注意注释中的的
behavior的参数的值 ==3 - 将信息存入了寄存器后,就BL进入
_lookUpImpOrForward源码,开始查找。
二、lookUpImpOrForward分析
进入C++源码定位到方法后,来分析一下_lookUpImpOrForward中的实现逻辑:
如图所示,整个_lookUpImpOrForward主要代码的含义已经标注完成了,现在先对其整体逻辑进行一下梳理,然后再对某些局部的细节展开来分析一下:
- 上半部分为准备工作:创建默认IMP、检查Cls的合法、递归实现Cls继承链和初始化
MetaCls,准备好相关类的rw为查找method_list作准备。 - 下半部分为判断查找:
realizeAndInitialize后与当前Cls相关的一系列Cls都已经完成初始化,进for先是if...else判断。- iOS会再次进入汇编
cache_getImp查找一次,内部逻辑依然是快速查找的CacheLookup。 - 其他则进入
getMethodNoSuper中二分遍历Cls的method_list。
- iOS会再次进入汇编
- 完成if...else如果依然未命中,此时
CurCls==Cls.SuperCls,父类再次进入cache_getImp查找。- 命中则
go done - 未命中则 继续循环
- 命中则
- for循环外的if逻辑,如图标注。 整体逻辑梳理如上,现在开始对其中的局部细节展开来分析。
2.1 forward_imp
由刚才对整体流程的分析可知,这个IMP是最后找完了整个继承链-直到NSObject还没有找到的时候返回的IMP,日常开发中在控制台见到它的时候是这个样子:
那么其中的 unrecognized... 的是怎么返回来的呢,现在就进入源码看看:
搜索后发现forward_imp 的真实实现也是汇编,进入可以看到逻辑并不复杂,使用adrp 读取了__objc_forward_handler 的地址并且存入了存放IMP的x17中,那么再搜索一下_objc_forward_handler,看看这个被加载的句柄具体是什么内容:
如图中标注,根据__OBJC2__ 做了方法区分,返回包括类、元类的标识符“+”、“-”,类名等,统统拼接好了,这就是日常开发中见到的那个unrecognized - IMP。
2.2 checkIsKnownClass
如官方是注释所说,此方法为了防止CFI攻击(暂时我也不知道具体是什么)、确保Cls的合法性和安全,所以方法的内部主要是去查当前运行的Cls是否在共享缓存、已加载Image的数据段、或者是否被obj_allocateClassPair创建的,且在Cls的Cache中存有 uint16_t witness标识用于验证。
- 使用witness去数据查询,大概率会返回Ture。
- 如果if判断失败的话,则去查存
allocatedClasses表。
2.3 realizeAndInitializeIfNeeded_locked
这个方法主要是为了接下来的for循环而处理cls及Cls.Superclss等一系列相关的。现在来看看其内部实现:
其内部主要就是2个if判断,同时也是涉及着isa关系图 中两个链条的实现及初始化:
- 首先是判断
cls->isRealized,判断的依据是data()->flag的31位,if内方法的目的就是实现当前Cls及SuperCls这个链条上的Cls.rw。- 对于OC来说,if内的方法会具体实现到
realizeClassWithoutSwift随后展开分析。 - 另一个分支,则是Swift。
- 对于OC来说,if内的方法会具体实现到
- 其次是判断
cls->isInitialized,判断的依据是data()->flags的29位,if内方法的目的就是初始化当前Cls及Metal这个链条上的Cls- if内的方法会具体实现到
initializeNonMetaClass随后展开分析。
- if内的方法会具体实现到
1. realizeClassWithoutSwift
- 截取了
realizeClassWithoutSwift中重要的、能够体现其主要逻辑的部分,通过之前对于类de数据结构分析(总) ,图中代码的意思应该是见文知意的。
2. initializeNonMetaClass
- 依旧截取了
initializeNonMetaClass中能够体现其主要逻辑的代码,从入口开始现在当前Cls的SuperCls开始初始化,一级一级往下初始化,初始化完成后没有什么问题则直接return。
2.4 getMethodNoSuper_nolock
接下来,看看getMethodNoSuper_nolock中是如何对method_list查找的:
因为method_array_t 是二维结构,所以在这里先有一层for循环,继续深入就是findMethodInSortedMethodList 了:
如图,使用二分查找对于method_list_t进行检索,其中的>>1就是二分的关键,在图中也已标注。其他逻辑相对也不难,只有2个点容易让人产生疑惑,如图中所标注,现在来详细解释一下:
-
命中目标
SEL后,为什么还要Probe--向前查找同名SEL,最后返回最靠前的?-
首先,注释中提到了,这个
Probe--和类别(Category) 有关,具体说就是类别中的同名方法会覆盖主类中的方法。 (同时当前问题或者图中这个查找算法也可以反过来解释:为什么最后编译的类别中的同名方法会覆盖主类中方法) -
其次,主类方法不是被覆盖了吗?类别的方法不是追加的吗?怎么还要
Probe--向前查找?这些问题关键在于:类别的method_list加入主类method_list时的方法attachLists()中的插入算法:开辟新的数组后,method_list的插入逻辑是倒叙的,也就是新的list在前,旧的在后。 (关于attachLists的分析会在之后分析到Category时做展开)
多数一句:
探索分析到这里时,起初不解为什么Probe--,只是通过注释知道和Category有关。在网上查找相关问题时,有的博客里写:
这里Probe--是因为要按照栈(Stack)的FIFO原则取值。类别是后覆盖了主类的的方法,类别的方法后入栈,所以要向前去取。
离大谱了!method_list_t继承自entsize_list_tt,其子类还有property_list_t、ivar_list_t。这些家伙的父类entsize_list_tt是拥有iterator的结构体啊,官方注释也有标注:Generic implementation of **an array **of non-fragile structs. 怎么会和Stack有关系呢??? -
-
未命中SEL的时候,为什么要做
Count--?-
--之后,可以将
base当前位置的占用减去,在下一次循环中将probe的位置移动至剩余查找量的2/1处。 -
画了个简单的草图,来理解一下:
-
三、总结
以上就是本篇对于objc_msgSend_uncached慢速查找流程的全面分析,除了对于源码逻辑的文字分析外,大部分源码的逻辑解释分析都附带到了截图中,同样需要认真看,同样需要认真看。
至此,OC消息机制中-关于IMP查找的所有源码就已经全部分析完了,接下来则进入关于方法的动态决议部分的分析。
篇中分析、记录的内容如有帮助,欢迎点赞、收藏、评论。如果错误,欢迎指出🙆🏻♂️。