前言
在前面的文章方法快速查找中,我们探究了Runtime
快速查找缓存的方法,当缓存没有找到时会进行慢速查找
。本文将对方法的慢速查找过程进行探究。
__objc_msgSend_uncached
- 方法快速查找找不到时,会走
MissLabelDynamic
方法,而这个方法对应传入的是__objc_msgSend_uncached
,我们来全局搜下:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
- 里面有两个方法,先来看下
TailCallFunctionPointer
TailCallFunctionPointer
.macro TailCallFunctionPointer
// $0 = function pointer value
br $0
.endmacro
这里只有一个跳转操作
- 再看看
MethodTableLookup
MethodTableLookup
.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
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0 // 将x0赋值,给x17
RESTORE_REGS MSGSEND
.endmacro
- 先看
mov x17, x0
,x0
是第一个寄存器,是接收返回值的,所以imp
肯定是在上面返回的,然后我们定位到_lookUpImpOrForward
,搜索发现只在这里,然后去掉_
后再搜索就找到C++中的代码:
这里有个疑问:为什么查找缓存用汇编写呢?
1. 汇编更接近机器语言,运行的速度比较快,更加动态化
2. 比较安全
3. 查找方法时,很多参数是动态的不确定的,C或C++
参数都是确定的,而汇编的参数是动态化的,刚好满足。
lookUpImpOrForward(慢速查找)
- 因为我们要寻找的目标是
IMP
的返回,所以我们直接找IMP
返回即可,在for (unsigned attempts = unreasonableClassCount();;)
这个循环里,有对IMP的赋值
,在分析主要流程之前,先来看看前面做了什么
查找准备
-
- 先判断类有没有加载:
if (slowpath(!cls->isInitialized()))
,如果没加载behavior |= LOOKUP_NOCACHE
- 先判断类有没有加载:
-
- 如果类已经加载,判断
是不是注册类
:checkIsKnownClass(cls)
- 如果类已经加载,判断
-
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE)
,这个方法是对类、父类、元类
的ro
和rw
进行赋值,是为了后面的for循环
查找做好准备。
循环查找
- 循环中的共享缓存不看,然后看
getMethodNoSuper_nolock
:
getMethodNoSuper_nolock
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
- 这里根据类拿到方法列表,然后从
beginLists
往endLists
开始遍历,每查找一次,都会调用search_method_list_inline
方法
二分查找
- 在
search_method_list_inline
里查看排序过的方法,最终找到了findMethodInSortedMethodList
,这是一个著名的二分查找算法:
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
二分查找
流程图如下
二分查找流程图
二分查找用例
我们举例来验证下二分查找流程:
-
- 数组
count
为8
,keyValue为6
,base = 0
- 数组
list: 1 2 3 4 5 6 7 8
期望 6
count = 8
base = 0
first = 0
查找过程如下流程:
这个情况只需要两次遍历就得到了结果,当计算的结果小于预期时,就会改变base值,继续查找
。
-
- 数组
count
为8
,keyValue为2
,base = 0
- 数组
list: 1 2 3 4 5 6 7 8
期望 2
count = 8
base = 0
first = 0
再来看看这个情况:
这里也是两次找到结果,当计算的结果大于预期时,base值不变,继续查找
- 通过这两个例子,能感觉到二分查找的魅力与精妙,感觉设计这个算法的人简直太厉害了。
cache_getImp(没找到imp)
如果上面查找没有找到imp
,会往下走,有方法imp = cache_getImp(curClass, sel)
// Superclass cache.
imp = cache_getImp(curClass, sel);
注释上是父类缓存
,curClass
怎么变成了父类呢,原来在判断的时候就赋值了if (slowpath((curClass = curClass->getSuperclass()) == nil))
,也就是说子类没找到,会去找父类,直到找到NSObject
为止。再来看看cache_getImp
方法,搜索发现在汇编代码
:
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
- 又找到了
CacheLookup
流程,也就是快速查找,找完然后再走C++
的慢速查找,但这个和之前不一样,:
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
LGetImpMissDynamic:
mov p0, #0 // 返回nil
ret
LGetImpMissConstant:
mov p0, p2
ret
END_ENTRY _cache_getImp
这里将#0
给p0
,也就是imp = nil
返回,然后再在父类中找。
log_and_fill_cache(找到imp)
如果循环遍历找到了imp,会走done
方法:
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES //共享缓存
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
抛开共享缓存不看,目标就在log_and_fill_cache
里
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cls->cache.insert(sel, imp, receiver);
}
- 然后我们就看到了熟悉的方法
insert
,也就是我们进行cache分析的insert
流程。 - 整个过程就是
准备查找条件
->循环查找方法列表
->进行二分查找
,没找到的话在父类再重新查找
,找到就insert到缓存
慢速查找流程图
整个慢速查找流程如下
总结
- 慢速查找缓存大致分为3步:
查找前准备(设置好ro、rw相关方法)
->遍历方法列表,每一次都进行二分查找
->父类中查找
- 比较难理解的应该是
二分查找
,当算法不太明白时,可以写一些实例,跟着步骤走就会豁然开朗。这个算法写的特别妙,大大降低了搜索时间。