前言
为了更加深入的理解方法查找中的快速查找,我们从汇编代码里面摸索了很久过程很枯燥,但是收获也挺多,汇编分析暂时告一段落,今天我们着重看下方法查找过程中的慢速查找;
快速查找汇编回顾
快速查找缓存的汇编代码已经在之前文章说过了,这里不再赘述,只对汇编代码过程中容易出错的点进行说明;
快速查找于慢速查找
- 通过查阅汇编源码
_bucketsAndMaybeMask重点
_bucketsAndMaybeMask里面存的数据包含低16位的_maybeMask和高48位的buckets_t pointer两部分;- 其中高48位是
buckets_t pointer;低16位是_maybeMask是capacity - 1; - 取
buckets_t的时候需要将bucketsMask & _bucketsAndMaybeMask才能得到;注意这里用的是bucketsMask不是_maybeMask!; bucketsMask是一个固定常数,而_maybeMask则是根据capacity - 1计算得出;
cache_hash 和 cache_next
cache_hash方法是首次哈希通过sel计算下标时候用到的方法;cache_next是二次哈希通过sel计算下标时候用到的;cache_hash内部的在计算前sel会先将sel转换成数值,然后>>7位,最后用处理后的sel & mask;这里用到的mask就是_maybeMask即capacity - 1;
首次哈希
- 首次哈希时,
cmd&mask得出当前bucket的index; - 再把
cmd&mask后左移4个byte位(左移16字节)呢?每个bucket都是IMP和SEL组成,共16个字节,左移4个单位相当于index*16就得到了当前index与buckets的首地址的偏移地址;
架构判断对代码走向的影响;
- 前文我们分析的汇编都是基于
Mac架构,实际上其他架构模式下整个快速查找的流程差别不大;但还是要注意某些宏定义和架构判断对整个流程的影响;例如下图的红色区域代码如果满足预编译条件,则对缓存扩容逻辑会产生较大影响;
真机查看汇编
真机查看汇编和通过源码查看汇编基本一致,唯一区别的是要根据寄存器当前的数据,来确定是否位是当前调用的信息;- 查看当前寄存器信息的
LLDB指令register read x0,register read x1,分别获取当前的x0和x1寄存器,一般情况下,参数都是存放在x0-x7这8个寄存器中; - 具体流程不再分析了;
objc_msgSend使用汇编语言的优势
- 由于更加接近及其语言,汇编方法执行效率更高;
- 由于汇编语言是底层语言,不容易像上层语言那样被hook,安全性更高;
- 汇编语言是基于地址来执行的,在面对不同的参数类型时具有更高的灵活性,也更加的动态化;
类方法慢速查找
慢速查找流程
- 查看自身的
methodList中是否存在当前SEL对应的IMP; - 自身查找不满足时,查找
父类的cache、查找父类的methodlist; - 如果所有父类都找不到则进行
消息动态决议、快速消息转发、慢速消息转发等操作;
从快速查找到慢速查找
- 通过分析我们知道
objc_msgSend的汇编代码在查找cache命中之后会执行CacheHit \Mode;未命中的情况下会执行MissLabelDynamic,而MissLabelDynamic对应的参数则是__objc_msgSend_uncached这个汇编方法; - 在
__objc_msgSend_uncached这个汇编方法的内部我们找到了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寄存器作为返回体,说明方法查找是从上一步_lookUpImpOrForward方法里获取的结果
RESTORE_REGS MSGSEND
.endmacro
- 由于
x0寄存器存放的是返回值信息,所以在MethodTableLookup方法里,x0寄存器出现的那行代码的上一行_lookUpImpOrForward极有可能就是查找imp的方法,是不是这样呢? - 全局搜索
lookUpImpOrForward发现是由c++方法实现的,查看lookUpImpOrForward源码,最终证明了我们的猜想; - 汇编方法调用C++方法时会在方法名前面添加
_(下划线);
慢速查找lookUpImpOrForward
lookUpImpOrForward方法内部是查找IMP的,所以我们最直接的办法就是找到处理IMP相关的地方,在查找之前做了一些准备工作:
- 包括初始化判断
if (slowpath(!cls->isInitialized())):进行一些初始化判断; checkIsKnownClass(cls);判断当前的class是否已经是注册类realizeAndInitializeIfNeeded_locked:对当前类的ro、rw进行处理主要是methodlist和propertylist;对superclass、metaclass进行处理;设置isa走位;只要有某一个类进行初始化,与他相关的metaclass、superclass都会进行初始化;主要作用是为了方法查找过程中对父类的信息进行处理,处理完以上信息为后面的方法查找做准备;
- 做完以上准备工作之后正式进入循环查找的流程,这里比较特殊的是,由于循环终止条件的缺失,导致当前循环相当于一个死循环,所以必须依靠终止语句才能跳出循环;循环中主要进行以下操作
- 再次查找一边缓存
curClass->cache.isConstantOptimizedCache(/* strict */true) Method meth = getMethodNoSuper_nolock(curClass, sel);二分查找流程查找curClass的methodlist,此时curClass = self;(curClass = curClass->getSuperclass()) == nil)二分查找自身未查到时候,执行这里;将curClass指向父类,若父类为空直接forward_imp,否则下面查父类的cahecache_getImp(curClass, sel)如果一直找不到无法执行done,就会执行到这里查父类的cache;log_and_fill_cache(cls, imp, sel, inst, curClass);//用找到的方法填充缓存,下次再调用方法就可以使用快速查找了; 3.慢速查找的代码流程如下
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();
//1:进行一些初始化判断
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
runtimeLock.lock();
//2:判断当前的class是否已经是注册类
checkIsKnownClass(cls);
//3: 对当前类的ro、rw进行处理主要是methodlist和propertylist;对superclass、metaclass进行处理;设置isa走位;只要有某一个类进行初始化,与他相关的metaclass、superclass都会进行初始化;主要作用是为了解决方法查找过程中对父类的信息进行处理;处理完以上信息后为后面的方法查找做准备;
//由于循环终止条件的缺失,导致当前循环相当于一个死循环,所以必须依靠下面的终止语句才能跳出循环;
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
runtimeLock.assertLocked();
curClass = cls;
//此处的for循环相当于死循环,主要靠下面的goto和break等退出语句进行处理;
// 4:当前方法是为了查找IMP的,所以当前循环是核心方法
for (unsigned attempts = unreasonableClassCount();;) {
//5:再次查找一边缓存
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
} else {
// curClass method list.
//6:重点方法:二分查找流程查找curClass的methodlist,此时curClass = self;
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
//查找到imp跳转到done,
goto done;
}
//7: 二分查找自身未查到时候,执行这里;将curClass指向父类,若父类为空直接forward_imp,否则下面查父类的cahe
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
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.
//8: 如果一直找不到无法执行done,就会执行到这里查父类的cache
imp = cache_getImp(curClass, sel);//这里如果通过cache_getImp查找不到,就会递归调用本方法,但是传进来的就是父类了,
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
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:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
//9: 用找到的方法填充缓存,下次再调用方法就可以使用快速查找了;
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
慢速查找流程图
慢速查找方法lookUpImpOrForward的全部流程图如下
二分查找细节解析
- 二分查找的使用前提是有序数组,这里methodlist中已经将方法进行排序了,数组内部的SEL值随着数组inde的增大而增大,所以我们可以用二分查找;
- 作为查找方法的算法,它的性能要求很高;
- 查找到方法时需要向前移动,查看前面的SEL是否也和目标SEL相等,直到找到位于数组最前面位置的SEL,这样做主要是为了当category中的方法覆盖了class中的方法时,保证取出的是category的方法;category的方法在加载进内存时是最先加载到methodlist中的,所以位于最前面; 4.二分查找method的代码及分析如下
{
ASSERT(list);
auto first = list->begin();
// base开始为0
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//假设长度位8,目标在第7个位置;
// >> 右移1位相当于/2;
//获取list的总长度;假设当前count = 8,满足count!=0,probe=0+8/2=4;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
//取出probe对应的IMP和keyValue对比
if (keyValue == probeValue) {
//查看当前probe的前一个和keyvalue是否相等,如果相等继续向前,直到不想等时停止,此时probe还没有执行--所以,当前的probe是list中第一个和keyvalue相等的方法;因为category的方法顺序排在类中的方法前面,这做是导致的结果是当分类中的方法和类中的方法重复时,取出的是分类中的方法而非类中的方法;
//你问我为什么知道是这样的结果?额,你看不到上面的注释吗?哈哈哈(This is required for correct category overrides.)
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
//如果keyvalue比probeValue大,(即要查找的方法还在数组后面位置)则base在之前的基础上+1;count--
if (keyValue > probeValue) {//count = 7;base=5
base = probe + 1;
count--;
}
}
return nil;
}
OC代码模拟二分查找
我们尝试用oc对这个算法进行实现,分析实现原理;
//调用方法,共17条数据,目标数据位于index=5的位置
[self findMethodInSortedMethodList:5 array:@[@(1),@(2),@(3),@(3),@(3),@(5),@(9),@(11),@(17),@(17),@(17),@(23),@(50),@(51),@(51),@(51),@(51)]];
- (NSNumber *)findMethodInSortedMethodList:(int)key array:(NSArray *)list {
int first = 0;
// base开始为0
int base = first;
int probe;
int keyValue = key;
int count;
for (count = list.count; count != 0; count >>= 1) {
probe = base + (count >> 1);
int probeValue = [list[probe] intValue];
if (keyValue == probeValue) {
while (probe > first && keyValue == [list[probe - 1] intValue]) {
probe--;
}
return @(probe);
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
二分查找原理总结;
- 每次查找时,最重要的是确定当前的目标位置,目标位置=起始位置+有效数据量的/2;
- 首次查找起始点为0,有效数据量为数据总量,
- 如果上次的目标位置取出来的数>实际要查找的数,则起始点不变,有效数据量变为上次有效数据量的一半,
- 如果上次的目标位置取出来的数<实际要查找的数,则起始点变为上次目标位置+1,有效数据量变为上次有效数据量-1后的一半;
二分查找实战
- 第一次二分查找
-
第一次分析,从
0开始,查找数据总数为17,所以目标位置是:0+17/2=8 -
第一次
base=0;count=17;probe=0;probe=base+count/2=0+17/2=8;list【8】= 17;5 < 17不做处理;
- 第二次分析
- 从
0开始,剩下的查找数据总数为8;目标位置是0+8/2=4 - 第二次
probe = 8,base = 0;count = 8;probe = base+count/2 = 0+4=4;,list【4】= 3;因为5>3,count—=7,base=pro+1=4+1=5; 因为找出来的数比实际的数小了,所以查找的起始位置要从当前查找位置向前移动1即4+1=5;剩下的查找数据总数也要减1,即8-1=7;
- 第三次分析
- 从
5开始,剩下的查找数据总数为7/2 =3;目标位置5+3/2=6; - 第三次
probe = 4,base = 5;count = 3;probe = base+count/2 = 5+3/2=6;list【6】 = 9;5<9不做处理;
- 第四次分析
- 从
5开始,剩下的查找数据总数为1,5+1/2=5;最终查找到目标是5 - 第四次
probe = 6,base = 5;count = 1;probe = base+count/2=5+1/2=5;list【5】= 5,满足条件;