开篇
好好学习,不急不躁。
在上篇文章中,主要研究了objc_msgSend的消息发送流程,分析的场景主要是在msgSend发送顺畅,缓存命中的情况下;那在缓存未命中的情况下,消息发送流程有是如何执行的呢?今天我们就来探析一下;
方法缓存
objc_msgSend 的消息发送过程中,如果要查找的sel,与查询到的sel 相同,则代表缓存命中,执行CacheHit
函数逻辑;如果缓存未命中,则执行MissLabelDynamic
函数逻辑;
从CacheLookup 传递的值可以看出对应的函数:
Mode —-> NORAML;
Function —-> _objc_msgSend;
MissLabelDynamic —-> _objc_msgSend_uncached;
既然MissLabelDynamic
函数,执行的是_objc_msgSend_uncached
流程,那么接下来,就主要分析_objc_msgSend_uncached
流程
msgSend 缓存未命中
.endmacro
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
MethodTableLookup
TailCallFunctionPointer x17
......
复制代码
在源码内,MethodTableLookup
、TailCallFunctionPointer x17
这两个函数比较重要;
TailCallFunctionPointer
函数,做了数据返回,也表示在它之前数据获取已基本完毕,所以我们重点看MethodTableLookup
函数;
通过逐层分析,最终得出,_lookUpImpOrForward
函数是一切的关键;
lookUpImpOrForward
全局搜索 _lookUpImpOrForward
函数,却发现在汇编的源码中,并没有_lookUpImpOrForward
的源码,表明_lookUpImpOrForward
函数,并不是使用汇编源码流程,尝试搜索lookUpImpOrForward
,却发现lookUpImpOrForward
流程,是以C++的源码形式存在;
那么问题来了,为什么缓存命中的时候,采用汇编方式,而缓存未命中时,执行的却是C++的方式?采用汇编有什么好处呢?
汇编是机器识别语言,对于设备来说,识别汇编语言执行起来是非常高效的,并且是非常安全的;
所以在缓存命中的时候,通过缓存直接查找方式是非常快速的,也就是我们通常说的,快速查找流程;
而在缓存未命中的时候,需要遍历所有方法查找,这个过程就比较缓慢,所以是一个慢速查找流程。系统就将该流程放在c/c++里;
慢速查找
慢速查找流程
:
1、首先查找当前类的method list,看看有没有当前sel对应的imp;
2、子类没有,再继续查找父类 method list;
3、父类没有,查找NSObject method list;
4、最后走到nil,跳出查找流程;
在lookUpImpOrForward
方法中,我们的目标就是获取imp,所以我们针对核心区代码进行解读;
method 二分法
深入查看getMethodNoSuper_nolock
方法,是如何进行二分法查找的。
而在getMethodNoSuper_nolock
并没有二分查找的痕迹,这是因为method list可能是一个二维数组,这个我们在之前讲解类方法获取的文章中讲解过;既然 method list 可能是二维数组,那么就无法在此处做二分法;
search_method_list_inline
获取的是 method_t,而search_method_list_inline
内部依据判断条件,执行findMethodInSortedMethodList
函数,那么我们着重分析这个函数;
findMethodInSortedMethodList
函数
源码内的二分法非常精妙,通过两次的右移与减减操作,能精确定位二分下标,同时也减少循环操作,提升效率;
举个例子说明此处的二分法:
method 查找结束后,通过method即可获取imp,接着执行 goto done
方法,进行下一步;
在done
方法内部,将会进行缓存写入,防止下一次再次获取时,再次执行二分法查找消耗内存,存入缓存同时也提高效率;
至此,消息的发送、查找,插入就形成一个闭环;
消息慢速查找流程,会通过继承链不断的查找,子类没有,查询父类,父类的查询流程,也一样存在快速查找与慢速查找;