底层探索 -- OC消息机制(二)lookUpImp慢速查找
本篇重点
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
作准备。下半部分为判断查找:
命中则
go done
未命中则 继续循环
iOS会再次进入汇编
cache_getImp
查找一次,内部逻辑依然是快速查找的CacheLookup
。其他则进入
getMethodNoSuper
中二分遍历Cls的method_list
。realizeAndInitialize
后与当前Cls相关的一系列Cls都已经完成初始化,进for先是if...else判断。完成if...else如果依然未命中,此时
CurCls==Cls.SuperCls
,父类再次进入cache_getImp
查找。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。
其次是判断
cls->isInitialized
,判断的依据是data()->flags
的29位,if内方法的目的就是初始化当前Cls及Metal这个链条上的Clsif内的方法会具体实现到
initializeNonMetaClass
随后展开分析。
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--
是因为要按照栈(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
有关系呢???首先,注释中提到了,这个
Probe--
和类别(Category) 有关,具体说就是类别中的同名方法会覆盖主类中的方法。 (同时当前问题或者图中这个查找算法也可以反过来解释:为什么最后编译的类别中的同名方法会覆盖主类中方法)其次,主类方法不是被覆盖了吗?类别的方法不是追加的吗?怎么还要
Probe--
向前查找?这些问题关键在于:类别的method_list
加入主类method_list
时的方法attachLists()
中的插入算法:开辟新的数组后,method_list
的插入逻辑是倒叙的,也就是新的list在前,旧的在后。 (关于attachLists
的分析会在之后分析到Category时做展开)未命中SEL的时候,为什么要做
Count--
?--之后,可以将
base
当前位置的占用减去,在下一次循环中将probe
的位置移动至剩余查找量的2/1处。画了个简单的草图,来理解一下:
三、总结
以上就是本篇对于objc_msgSend_uncached
慢速查找流程的全面分析,除了对于源码逻辑的文字分析外,大部分源码的逻辑解释分析都附带到了截图中,同样需要认真看,同样需要认真看。
至此,OC消息机制中-关于IMP查找的所有源码就已经全部分析完了,接下来则进入关于方法的动态决议部分的分析。
篇中分析、记录的内容如有帮助,欢迎点赞、收藏、评论。如果错误,欢迎指出????????♂️。
作者:红扑扑的小脸儿
链接:https://juejin.cn/post/7066360938613440543