在上一篇文章中介绍了在汇编部分的缓存快速查找流程。由于首次调用
或者缓存扩容
等问题导致的缓存查找失败,就需要进入慢速查找流程
.
objc_msgSend慢速查找
慢速查找入口-汇编部分
- 在快速查找流程无法找到对应缓存的时候,会跳到
CheckMissJumpMiss
这个macro中并且走到__objc_msgSend_uncached
这个函数中。
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup //跳转
TailCallFunctionPointer x17 //寄存器调用
END_ENTRY __objc_msgSend_uncached
- 继续探索
MethodTableLookup
的实现,发现实现很长,但是都是一些寄存器的操作等,只看我们关心的b、bl
.macro MethodTableLookup
...
// 省略影响跟流程的代码
bl _lookUpImpOrForward
...
- 继续搜索
_lookUpImpOrForward
发现找不到对用的实现了。苹果编码有一个习惯通过前缀增加、减少_
,来实现多种语言的切换。
通过调用lookUpImpOrForward
,就来到了c 部分。
通过调试来跟踪流程
前文中的流程跳转有一部分猜测的成分,现在通过调试来验证一下之前的猜测.
- 打开断点,找到目标调用
- 打开堆栈信息选项
- 按住control 点击 Step info
- 点击下一步来到
__objc_msgSend_uncached
- 最终调用
lookUpImpOrForward
,而且给出了c 函数的位置
通过编译调试进一步验证了之前的猜测。
慢速查找c 流程
代码语言:javascript复制IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//参数准备
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
//再进行一次缓存的查找
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
runtimeLock.lock(); //加锁
// TODO: this check is quite costly during process startup.
//判断该类是否是已知类
//这个操作需要从已知类表中做耗时的查找动作;推测以后苹果会优化点
checkIsKnownClass(cls);
//判断当前类是否加载到内存中(isa和rw信息是否存在)。
if (slowpath(!cls->isRealized())) {
//如果不存在进行类的加载,并且会递归加载所有父类、元类。为了确定类的父类、isa链
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
//判断类是否初始化,同样是递归判断所有
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
//对类进行初始化,并且会递归加载所有父类、元类。为了确定类的父类、isa链
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked(); //解锁
//循环查找,无跳出条件
for (unsigned attempts = unreasonableClassCount();;) {
//使用二分法在当前类的方法列表methodList中查找该方法
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
//本质就是查找方法的imp实现
imp = meth->imp;
goto done;
}
//1. 将当前类切换为父类
//2. 如果当前类的父类是nil,也就是在NSObject中都没有找到则直接跳出循环
if (slowpath((curClass = curClass->superclass) == nil)) {
imp = forward_imp;
break;
}
// 如果父类链中存在循环,则停止
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
//对父类进行方法查找
//1.汇编缓存快速查找
imp = cache_getImp(curClass, sel);
//如果父类没找到,首次会进入动态方法解析
if (slowpath(imp == forward_imp)) {
break;
}
//在父类中找到了该方法
if (fastpath(imp)) {
goto done;
}
}
//每个类首次都会进行一次方法动态解析
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;
}
//返回查询结果imp
return imp;
这就是整个消息imp查询闭环流程(快、慢)。
慢速查找流程图
objc_msgSend慢速流程.png
cache_getImp没有发生递归
代码语言:javascript复制STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0
CacheLookup GETIMP, _cache_getImp
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
- 可以看到CacheLookup的第一个参数是
GETIMP
- GETIMP下并没有调用
__objc_msgSend_uncached
,所以并不会产生递归,只会查找当前类的缓存
方法没有找到,而且没有实现方法解析
extern void _objc_msgForward_impcache(void);看到extern
就知道它又是汇编代码编写的了
STATIC_ENTRY __objc_msgForward_impcache
//跳转到 __objc_msgForward
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
//定位
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
//寄存器操作
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
- 但是全局搜索
__objc_forward_handler
没有找到下文了,猜测它是c 编写,所以取一个_继续搜索
- 相信这段话已经很熟悉了吧?平时开发应该见过无数次了。
MethodList(有序数组)二分查找
以上已经解释了慢速查找的整个流程,现在对MethodList二分查找
的实现做一个解释。(核心方法findMethodInSortedMethodList
)
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
ASSERT(list);
//当前list是有序的,在类创建的时候就已经进行了加载
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key; //key 等于 say666
uint32_t count;
//base相当于low,count是max,probe是middle,这就是二分
for (count = list->count; count != 0; count >>= 1) {
//从首地址 下标 --> 移动到中间位置(count >> 1 右移1位即 count/2 = 4)
probe = base (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
//如果查找的key的keyvalue等于中间位置(probe)的probeValue,则直接返回中间位置
if (keyValue == probeValue) {
// -- while 平移 -- 找出分类重名方法
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
//找出分类重名方法
//如果是两个分类,就看谁先进行加载
probe--;
}
return (method_t *)probe;
}
//如果keyValue 大于 probeValue,就往probe即中间位置的右边查找
if (keyValue > probeValue) {
base = probe 1;
count--;
}
}
return nil;
}
示例图:
1. 向左查询
8中找1
2. 向右查询
8中找7