iOS底层探索之Runtime: lookUpImpOrForward慢速查找分析
Posted 卡卡西Sensei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS底层探索之Runtime: lookUpImpOrForward慢速查找分析相关的知识,希望对你有一定的参考价值。
1. 回顾
ios底层探索之Runtime(一):运行时&方法的本质
iOS底层探索之Runtime(二): objc_msgSend&汇编快速查找分析
在前面的文章中介绍了消息发送(objc_msgSend
)流程,主要是汇编快速查找cache
的过程,并对汇编源码进行了分析,本章内容主要分析慢速查找_lookUpImpOrForward
流程。
2. _lookUpImpOrForward
在汇编的快速查找没有找到缓存,就会进入__objc_msgSend_uncached
,在__objc_msgSend_uncached
里面最主要的是对MethodTableLookup
的处理。
2.1 MethodTableLookup
x0
寄存器里面存的是imp
,并赋值给x17
,x0
是第一个寄存器也是返回值的存储位置,如果imp
在x0
里面,必将做一件事情,就是返回,那么结果一定是在bl _lookUpImpOrForward
执行后的返回值里面,也就是我们要找的imp
存储的地方,所以接下来的重点就是_lookUpImpOrForward
。bl
:b
是跳转,l
是链接寄存器,将下一条指令的地址保存到lr
寄存器中,也就是把(mov x17, x0)
的指令地址保存在lr
中,当_lookUpImpOrForwar
执行完以后,执行lr
寄存器中的地址。_lookUpImpOrForward
找到imp
赋值给x17
寄存器
_lookUpImpOrForward
在源码里面没有找到汇编的实现,因为_lookUpImpOrForward
不是汇编写的,是C++
写的,所以去掉下划线就可以搜索🔍找到了
在
lookUpImpOrForward
的函数实现里面,确实发现了,lookUpImpOrForward
返回的是imp
,也就有验证了上面👆的汇编分析
缓存找不到了,就进入
慢速查找
流程,遍历method_list
方法列表,遍历是个耗时间的流程,所以就放入了C++
中实现,下面重点分析lookUpImpOrForward
👇
2.2 isKnownClass
lookUpImpOrForward - > checkIsKnownClass(cls) -> checkIsKnownClass - > isKnownClass
- 查询当前的类是否注册到缓存列表中
isKnownClass(Class cls)
{
if (fastpath(objc::dataSegmentsRanges.contains(cls->data()->witness, (uintptr_t)cls))) {
return true;
}
auto &set = objc::allocatedClasses.get();
return set.find(cls) != set.end() || dataSegmentsContain(cls);
}
lookUpImpOrForward -> realizeAndInitializeIfNeeded_locked -> realizeClassMaybeSwiftAndLeaveLocked -> realizeClassMaybeSwiftMaybeRelock -> realizeClassWithoutSwift
- 对
rw
、ro
进行处理
auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro();
ASSERT(!isMeta);
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
}
cls->cache.initializeToEmptyOrPreoptimizedInDisguise();
- 类,元类是否初始化注册
- 为什么要对类,元类进行初始化呢?
我在iOS底层探索之类的结构(上):ISA文章中已经介绍了isa
的走位,和元类
的继承关系。当对象调用方法的时候,判断当前类是否初始化,父类、元类是否初始化。目的是,如果当前类中没有实现方法,就去父类查找。如果元类中没有实现类方法,就去根元类查找。递归操作,遍地开花。
到底是怎么递归,怎么循环找方法的呢?请耐心往下看
- lookUpImpOrForward->for循环流程
我的天哪!开什么玩笑啊?这是循环吗?不要蒙我,我可是学过编程的人啊,循环有三个条件语句的啊!这就一个,后面两个都没有啊!
靓仔,你没有看错,这确实是循环,死循环!
这真的是
for
循环,只是循环体里面,有goto
,break
等语句打破死循环
3. 慢速查找分析
3.1 慢速查找流程大纲
- 查找自己方法列表
Method_list
->sel-imp
- 父类中查找 ->
NSObject
->nil
-> 跳出循环
大概就是这么个流程,那么我们下面去验证下
3.2 分析源码
进入for
循环首先就是一个if
判断,是否有共享缓存。
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
为什么又要去缓存里面查找啊?之前不是已经
汇编查找
过了啊?因为在操作ro/rw
的时候有可能写入了方法,所以这时候再去查看一遍,以防万一。
那么如果没有写入呢?没有就没有呗!那就继续往下执行代码。
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
- 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;
}
3.3 二分查找分析
在分析之前我们玩个小游戏,《猜猜猜》
在一次户外活动中,有编号1到100的盒子,其中有一个里面有奖品,猜是几号盒子有奖品,一共有五次机会。是你,你会怎么猜呢?下面是活动中胜出的猜测方法。
第一次猜: RENO:50 KC:小了
第二次猜: RENO:75 KC:大了
第三次猜: RENO:60 KC:大了
第四次猜: RENO:55 KC:对了
KC: 一共五次机会,第四次就猜中了,厉害啊!
这就是著名的二分查找法
(Binary Search),也叫折半查找
。下面面源码里面findMethodInSortedMethodList
就是这种算法实现的,可能刚刚那个游戏,你还无感知二分查找的魅力,看完下面👇的分析,你就能感知了。
search_method_list_inline - > findMethodInSortedMethodList - >
非M1电脑的找big的
- findMethodInSortedMethodList
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
,count >>= 1
就是右移1
位,相当于二进制1000
,变成0100
,count =4
-
probe = base + (count >> 1)
: 就是首地址base
加上偏移
。 -
if (keyValue == probeValue)
: 两个值相等的时候,if
判断里面的while
循环是对分类方法
进行处理,类和分类有可能同时实现了相同的方法,probe--
就是取分类的方法,因为排好序了,分类方法是排在前面一个位置,最后return &*probe
返回方法的地址. -
if (keyValue > probeValue)
: 大于中间值的情况,base = probe + 1
,就是4 + 1
,base
等于5
,count--
之后count
变为7
,进入下一次循环 -
7
右移变为3
,完美的避开了4
,因为4
已经比较过了,这是巧合吗?这就是算法的魅力
,我只能说苹果牛逼
!
-
上面已经执行过一次了,
4
不符合,那么范围缩小到5
到8
区间,那么只能取6
或者7
进行比较了。 -
循环的话又执行
probe = base + (count >> 1)
,base
上面算过了等于5
,count
等于3
,再count >> 1
之后count = 1
,probe
就等于6
。到这里我直呼,好家伙,好牛逼啊!完美的卡在了区间内。count >> 1
两次之后卡的这么完美,我只能再一次说苹果牛逼,佩服!佩服啊!
苹果工程师把二分查找
,用到了极致啊!不愧是世界第一市值的牛逼公司!
方法缓存查找到了就执行,
goto done
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
既然找到方法了,不可能再去执行二分查找了,就会调用log_and_fill_cache
方法,把它写入缓存中,提高下次查找速度。
- log_and_fill_cache
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);
}
- insert插入缓存
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);
}
4.总结
- 汇编的快速查找如果找不到缓存,就会进入
__objc_msgSend_uncached
,再到_lookUpImpOrForward
。 _lookUpImpOrForward
为什么不用汇编实现呢?
遍历method_list
,是个耗时间的流程,所以就放入了C++
中实现。findMethodInSortedMethodList
使用二分查找算法,提高查找效率
更多内容持续更新
🌹 喜欢就点个赞吧👍🌹
🌹 觉得学习到了的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹
以上是关于iOS底层探索之Runtime: lookUpImpOrForward慢速查找分析的主要内容,如果未能解决你的问题,请参考以下文章
iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析
iOS开发底层之RuntimeObjc_msgSend探究 - 08