在快速查找流程中,如果缓存命中了还好说,那么如果命不中呢,就会到我们的objc_msgSend
慢速查找流程,这篇文章就好好来分析是怎么进行慢速查找的
快速查找中如果没有找到方法实现,发现无论是CheckMiss
还是JumpMiss
,最终都会走到__objc_msgSend_uncached
汇编函数,如图
然后我们command+F 搜索一下__objc_msgSend_uncached
这玩意干了啥
找到之后,我们发现它做了一件事件,就是进行了方法列表的查找。然后我们接着搜索MethodTableLookup
,找到其源码实现
前面的准备工作汇编我们不需要管,关键是bl _lookUpImpOrForward
这里跳转到_lookUpImpOrForward
,然后搜索_lookUpImpOrForward
发现找不到了,那么去哪里了呢?
接下来我把项目跑起来,通过断点来找到它
首先,在main函数中[person sayHello]处打一个断点,然后我们打开汇编调试,在顶部状态栏选择Debug -- Debug worlflow -- 勾选Always show Disassembly,
运行程序
运行完了之后,我们进到objc_msgSend
里面去,按住control
,点中间调试按钮(stepinto)就进去了
来到objc_msgSend里面后我们发现下面有一个_objc_msgSend_uncached
,看到这里,果断打个断点,然后接着往里面进
走进来终于找到了我们的lookUpImpOrForward
在objc-runtime-new.mm
里面第6099行
,这就很爽了,我们直接找过去就行了。这里补充一点,细心的朋友可能会发现下划线去哪里了, 这是因为从我们的汇编到C++会少一个下划线
,从C++到C会再少一个下划线
,所以我们在汇编中去查找C/C++方法时要把下划线给去掉
明白了这一点之后我们接下来就全局搜索lookUpImpOrForward
,就来到了objc-runtime-new.mm
里面的lookUpImpOrForward
, 这里面是通过C/C++
写的,看起来还是要比汇编舒服一点
还是老规矩,我把这里面的代码大致翻译一下,看我分析之前,大家也可以边打断点边看我注释,这样流程更加的清晰
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
// 定义的消息转发
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// 快速查找,如果找到则直接返回imp
// 这个地方为什么又进行了快速查找? 其目的是为了防止多线程操作时,刚好调用函数,就可以有缓存了
// Optimistic cache lookup
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
// 快速查找在缓存里面找到了imp,直接返回imp
if (imp) goto done_nolock;
}
// 加锁,目的是保证读取的线程安全
runtimeLock.lock();
// TODO: this check is quite costly during process startup.
// 判断是否是一个已知的类:判断当前类是否是已经被认可的类,即已经加载的类
checkIsKnownClass(cls);
// 判断类是否实现,如果没有,需要先实现,此时的目的是为了确定父类链,方法后续的循环
// 这个地方没有imp,不是重点
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
// 判断类是否初始化,如果没有,需要先初始化
// 这个地方没有imp,不是重点
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked();
curClass = cls;
// ------------重点在for循环里面----------------
// unreasonableClassCount -- 表示类的迭代的上限
//(猜测这里递归的原因是attempts在第一次循环时作了减一操作,然后再次循环时,仍在上限的范围内,所以可以继续递归)
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
// 去当前类的方法列表查找(采用二分查找算法),如果找到,则goto done,将方法缓存到cache中
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
// -------- 如果二分查找找自己没有找到,那么就开始找父类的缓存了 ----------
// 注意, 这里将curClass = superclass 把父类赋值给了当前类了,并判断父类是否为nil
if (slowpath((curClass = curClass->superclass) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
// 父类全部找完了之后,父类为nil了, 把imp赋值为forward_imp
imp = forward_imp;
// 赋值完了之后会退出本次循环,说明父类也没有这个方法
break;
}
// Halt if there is a cycle in the superclass chain.
// 如果父类链中存在循环,则停止
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
// 找父类的缓存。
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// 如果在父类中找到了forward,则退出循环,调用此类的方法解析器
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
// 如果在父类中,找到了此方法,将其存储到cache中
goto done;
}
}
// No implementation found. Try method resolver once.
// 在自己和父类都没有找到方法实现,则会来到动态方法决议
if (slowpath(behavior & LOOKUP_RESOLVER)) {
// 这里是此次动态方法决议的控制条件
behavior ^= LOOKUP_RESOLVER;
// 动态方法决议,给一次处理的机会
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
// 存储到缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
// 解锁
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
这个代码流程分析的话我觉得要比上一篇文章的汇编要容易看懂一些。不过这里还是把整个慢速查找的流程总结一下
总结:
1、 首先cache
缓存中快速查找没有找到,则进入慢速查找lookUpImpOrForward
里面。来到这里之后还会进行快速查找一次,防止多线程操作时,刚好调用了此函数,这样就可以直接从缓存中快速查找, 不用再进行慢速查找了
2、判断cls
, 是否是已知类
。 然后该类是否实现
,如果没有,则需要先实现
,此时实例化的目的是为了确定父类链、ro、以及rw等
,为后面的数据读取查找做好准备。
3、 来到for循环,慢速查找的重点流程 :
3.1: 去当前类的方法列表查找
(采用二分查找算法),如果找到,则goto done,将方法缓存到cache中,如果没找到则开始找父类
3.2: 如果父类找到了imp
,则直接返回imp
,执行cache写入流程
,如果循环完所有的父类还没找到,最终会找到nil,父类为nil的话就会走到imp = forward_imp
,跳出当前循环
4、 当前类和父类都没找到imp
, 就会来到动态方法决议,给一次机会重新进行查询,如果进行处理了,能拿到imp
,就不会崩溃。
看完我这个总结,再去看我源码里的注释,我觉得应该非常清晰了,这就是整个方法的慢速查找流程。
在分析完objc_msgSend慢速查找之后,我再补充两个知识点
二分法算法
首先第一个知识点是查找方法列表的时候用的二分法查找,我们先来找到二分法算法的源码
这个代码我觉得非常的简单, 没有必要再一个个注释了,如果有看不懂的可以断点一试就明白了,也可以给我留言,我收到后会及时回复大家,我说一下大致的算法流程:
1、 拿到方法列表的第一个first和总数,然后开始遍历循环。注意,方法列表里面的数值是递增的,有序的
2、进到循环之后,probe = base + (count >> 1);
这句代码意思是首地址 位移 (count >> 1)
相当于从第一个元素移动到了中间
3、 移动到了中间之后,拿keyValue == probeValue
进行对比,如果等于,则返回method_t
。注意,这里有一个 probe--
,意思是排除分类里面名字一样的方法。
4、如果keyValue
大于 probeValue
,即在(probe +1) 到 (count--)
之间查找,同时count
每循环一次,右移一位,双数减半,单数减半再减一,比如8右移一位是 8/2 = 4,7右移一位是 7/2 - 1 = 3。然后再回到第二个步骤进行查找
5、如果keyValue
小于 probeValue
,即在1 - probe
之间继续取中间位置进行查找,同时count
每循环一次,右移一位。然后再回到第二个步骤进行查找
6、 一直循环到count
为0
,probeValue
为1
还没找到,就返回nil
了
消息转发
明白了二分法算法之后,我们最后再补充一个知识点,就是动态方法决议之后,会来到消息转发_objc_msgForward_impcache
这里,这个是汇编实现的。
然后会走到__objc_msgForward
,走到这里之后往下走,会来到__objc_forward_handler
,我们搜索__objc_forward_handler
找不到,因为汇编加了下划线,我们去掉一个下划线试试,搜索_objc_forward_handler
,就找到了C++
文件里面,然后最终走到了objc_defaultForwardHandler
里面。这就是一直都没有找到实现的方法,崩溃时报的错误提示。
最后,来一张消息转发机制流程图,为下一篇文章做准备