前言
根据上篇文章iOS底层探究--------Runimte 运行时&方法的本质中,通过汇编对objc_msgSend进行分析。当在缓存中找到我们要查找的方法时,进入的是缓存命中--CacheHit,直接返回找到的方法,但是假如没有找到,就会执行MissLabelDynamic(也就是__objc_msgSend_uncached)。
方法的查找,分为两步:
- 汇编缓存查找 ---- 快速查找;
MethodList查找(方法库里面查找) ---- 慢速查找(不断遍历方法库); 在上篇文章中,已经详细的探究了汇编缓存查找。如果在汇编缓存中找不到对应方法,那么将会到MethodList里面再去查找,也就是到C++底层库里面找。
那么这里就有疑问了,为什么方法缓存要用汇编写,而不是C++写了?
- 汇编语言更接近机器语言,执行起来,速度快、效率高;
- 安全,相同的结果,可能有很多种过程实现,被
hook的概率小; - 更加动态化,因为在
C++中查找,如果遇到参数未知(如:动态添加的可变参数),就不能进行精确的查找,但是汇编却并不要求这样。
那么接下来,我们就验证在汇编缓存找不到对应方法后,是去MethodList里面查找的。
资源准备
- objc源码:多个版本objc
进入主题
汇编如何切入到C++
因为不断遍历MethodList是十分耗时的过程,所以,就放到了C++里面进行。
通过__objc_msgSend_uncached传值imp
方法的查找,是通过方法对应的imp去匹配的,所以,想要在MethodList里面查找方法,一定是有该方法的imp的。
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
//---- ②要想查找方法,imp必不可缺,所以imp必然存储在MethodTableLookup里面(重点)
MethodTableLookup
//---- ①TailCallFunctionPointer是直接返回$0的,$0相当于查找时的起点了
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
MethodTableLookup切入C++方法
由于MethodList是由C++实现的,所以,查找也是需要进入到C++方法里面去。
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
//---- ②这里执行的跳转的操作,那么lookUpImpOrForwar对应着是C++的一个API方法了。
bl _lookUpImpOrForward
// IMP in x0
//---- ①因为0x是第一个寄存器,是返回值的存储位置,那么必然就会有返回的操作,和切入到C++方法完全不着边
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
在objc源码中,全文索引lookUpImpOrForward关键词就能得到:
那么这样就有
汇编进入到了C++。
简单介绍lookUpImpOrForward方法
源码太长,就把实现的判断方法给粘贴上了,{...}里面的内容折叠起来。lookUpImpOrForward方法是带返回值的IMP,所以方法的核心是在①处位置。
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;//---imp初始化
Class curClass;
runtimeLock.assertUnlocked();
//---- 初始化判断
if (slowpath(!cls->isInitialized())) {...}
runtimeLock.lock();
//---- 检查class是否注册到当前的缓存表里面(注册类)
checkIsKnownClass(cls);
//----子类、父类、元类的初始化和实现
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
runtimeLock.assertLocked();
curClass = cls;
//---- 这个for循环返回了不同情况下得到的imp(方法的核心)
for (unsigned attempts = unreasonableClassCount();;) {...}
if (slowpath(behavior & LOOKUP_RESOLVER)) {...}
done:
//---- 在ro和rw的methodlist里面找到该方法之后,把方法加入到缓存中,方便下次快速查找
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {...}
done_unlock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {...}
return imp;
}
源码回溯:
imp初始化;- 判断类是否初始化;
- 检查
class是否注册到当前的缓存表里面(注册类); - 通过
for循环得到的imp; - 最后把
imp返回出去;
对realizeAndInitializeIfNeeded_locked简析
这就像一个连锁反应,当对象调用方法的时候,马上判断当前的类是否初始化,如果已经初始化,就判断当前类的父类以及元类是否初始化。当父类和元类在初始化的时候,这两者的内部又继续分别调用各自的父类或元类进行初始化,这就相当于一个递归的操作。如下图(前面分析isa的文章有):
只要其中有一个初始化了,那么跟这个有关系的所有的环节,都要初始化,相当于遍地开花。
之所以这么做,就是为了找方法,就比如,当某个实例方法在子类找不到,就可以去父类找,父类找不到,就去父类的父类找。。。这样逐级查找,所以需要逐级的初始化。
相同的,如果是类方法,就在元类里面进行逐级查找。
在iOS底层探究--------cache分析一文中,类里面的ro和rw里面有propertylist和methodlist,正因为在realizeAndInitializeIfNeeded_locked()方法里面,ro和rw做了准备工作,所以,才能在接下来的for循环( for (unsigned attempts = unreasonableClassCount();;){...} )中来查找对应方法的imp。
for循环的递归查找
我们先来梳理下,我们所知道一般的查找流程(也就是慢速查找流程):
-
先查找自己:
methodlist-->sel -- imp; -
如果没找到,就到父类查找:
父类-->NSObject-->nil--> 跳出去了。 如源码,在for循环中:源码回溯:
-
①、先在缓存中查找一次,因为是为了防止所查找的方法,在类初始化的时候,就已经加入缓存中了,所以先找一遍,以防万一。在缓存中找到了,就直接返回imp,没找到就进入②; -
②、如果在缓存中没找到,就得在methodlist里面进行二分法查找。如果找到了,就直接写入缓存中去,方便下次在缓存中进行快速查找,没找到就进入③; -
③、如果当前子类找不到,就直接到父类里面找,父类里面查找也分为快速和慢速两个流程,首先是快速流程,那就得真机运行,在汇编里面找。快速流程找不到,再进行慢速流程找。如果父类先快速查找,没找到,再慢速查找,还是没找到,就去父类的父类里面,再进行快速和慢速查找,以此类推,所以是一个递归的过程。如果整个递归的过程还是找不到,那么就进入④; -
④、如果所有的父类都找完了,还是没有的话,会返回一个外传的imp
二分法查找方法
先来个简易的二分法查找流程
回到源码中,分析其中的
二分法查找流程:
源码回溯:
-
假设条件:的有
8个方法,那么count = 8,正确的方法在7号位置; -
第一次
for循环:base = 0,count = 8,其二进制:1000,此时查找区间就是(0,8)。计算probe = base + (count >> 1) = 0 + (1000 >> 1) = 4;比较传入方法value和 遍历的方法的value的大小,如果是等于,那么就直接返回该方法,如果是大于,计算base = base = probe + 1 = 4 + 1 = 5,count-- = 7;此时的查找区间(5,8),那么就意味着只能在6和7里面取值了; -
第二次
for循环:base = 5,count = 7,count先执行for循环的count >>= 1,那么就是:0111 >>= 1,得到:count = 0011 = 3,再次计算probe = base + (count >> 1) = 5 + (0011 >> 1) = 6。接着往下执行,当传入方法value大于 遍历的方法的value时,计算base = base = probe + 1 = 6 + 1 = 7,count-- = 3 - 1 =2;还是没找到,接着进行第三次for循环; -
第三次
for循环:base = 7,count = 2,count先执行for循环的count >>= 1,那么就是:0010 >>= 1,得到:count = 0001 = 1,计算probe = base + (count >> 1) = 7 + (0001 >> 1) = 7,最终找到7号方法,结束循环;
看到这里,有童鞋就会问了,要是前面列举的都是大于和等于的情况,如果是小于了,又是怎样了?
-
假设条件:的有
8个方法,那么count = 8,正确的方法在2号位置; -
在第一次循环中,计算
probe = base + (count >> 1) = 0 + (1000 >> 1) = 4,如果传入方法value小于 遍历的方法的value,不会走等于和大于的判断,base一直为0,那么就只执行count >>= 1,count = 4,此时还没找到,就进行第二次for循环,此时,计算probe = base + (count >> 1) = 0 + (0100 >> 1) = 2,那就直接找到了方法。 -
注:这里的只是用
count = 8作为例子运算,但是在实际当中,count可能是个很大的数值,执行多次for循环。还有注意,count>>1和count >>= 1,是有区别的,前者并没有赋值运算,所以count的值不改变,后者才有赋值运算,count的值才改变
二分法查找到的方法写入缓存log_and_fill_cache
源码回溯:
- 方法的写入是在第一次查找调用方法的时候,如果缓存里面没有,就开始进行慢速查找,慢速查找必然是在
rw和ro的methodlist里面进行,如果这里面都没有,就回报错。如果找到了该方法,就回在缓存里面insert,那么下次查找的时候,CacheHit缓存命中。