iOS 底层探索09——慢速方法查找

450 阅读10分钟

前言

为了更加深入的理解方法查找中的快速查找,我们从汇编代码里面摸索了很久过程很枯燥,但是收获也挺多,汇编分析暂时告一段落,今天我们着重看下方法查找过程中的慢速查找;

快速查找汇编回顾

快速查找缓存的汇编代码已经在之前文章说过了,这里不再赘述,只对汇编代码过程中容易出错的点进行说明;

快速查找于慢速查找

  1. 通过查阅汇编源码

_bucketsAndMaybeMask重点

  1. _bucketsAndMaybeMask 里面存的数据包含低16位的_maybeMask和高48位的buckets_t pointer两部分;
  2. 其中高48位是buckets_t pointer;低16位是_maybeMaskcapacity - 1
  3. buckets_t的时候需要将bucketsMask & _bucketsAndMaybeMask才能得到;注意这里用的是bucketsMask不是_maybeMask!;
  4. bucketsMask是一个固定常数,而_maybeMask则是根据capacity - 1计算得出;

cache_hashcache_next

  1. cache_hash方法是首次哈希通过sel计算下标时候用到的方法;cache_next是二次哈希通过sel计算下标时候用到的;
  2. cache_hash内部的在计算前sel会先将sel转换成数值,然后>>7位,最后用处理后的sel & mask;这里用到的mask就是_maybeMaskcapacity - 1

首次哈希

  1. 首次哈希时,cmd&mask得出当前bucket的index;
  2. 再把cmd&mask左移4个byte位(左移16字节)呢?每个bucket都是IMPSEL组成,共16个字节,左移4个单位相当于 index*16就得到了当前index与buckets的首地址的偏移地址;

架构判断对代码走向的影响;

  1. 前文我们分析的汇编都是基于Mac架构,实际上其他架构模式下整个快速查找的流程差别不大;但还是要注意某些宏定义和架构判断对整个流程的影响;例如下图的红色区域代码如果满足预编译条件,则对缓存扩容逻辑会产生较大影响image.png

真机查看汇编

  1. 真机查看汇编和通过源码查看汇编基本一致,唯一区别的是要根据寄存器当前的数据,来确定是否位是当前调用的信息;
  2. 查看当前寄存器信息的LLDB指令register read x0register read x1,分别获取当前的x0x1寄存器,一般情况下,参数都是存放在x0-x78个寄存器中;
  3. 具体流程不再分析了;

objc_msgSend使用汇编语言的优势

  1. 由于更加接近及其语言,汇编方法执行效率更高
  2. 由于汇编语言是底层语言,不容易像上层语言那样被hook,安全性更高
  3. 汇编语言是基于地址来执行的,在面对不同的参数类型时具有更高的灵活性,也更加的动态化

类方法慢速查找

慢速查找流程

  1. 查看自身的methodList中是否存在当前SEL对应的IMP
  2. 自身查找不满足时,查找父类的cache、查找父类的methodlist
  3. 如果所有父类都找不到则进行消息动态决议快速消息转发慢速消息转发等操作;

从快速查找到慢速查找

  1. 通过分析我们知道objc_msgSend的汇编代码在查找cache命中之后会执行CacheHit \Mode未命中的情况下会执行MissLabelDynamic,而MissLabelDynamic对应的参数则是 __objc_msgSend_uncached这个汇编方法;
  2. __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
  1. 由于x0寄存器存放的是返回值信息,所以在MethodTableLookup方法里,x0寄存器出现的那行代码的上一行_lookUpImpOrForward极有可能就是查找imp的方法,是不是这样呢?
  2. 全局搜索lookUpImpOrForward 发现是由c++方法实现的,查看lookUpImpOrForward源码,最终证明了我们的猜想;
  3. 汇编方法调用C++方法时会在方法名前面添加_(下划线);

慢速查找lookUpImpOrForward

  1. lookUpImpOrForward方法内部是查找IMP的,所以我们最直接的办法就是找到处理IMP相关的地方,在查找之前做了一些准备工作:
  • 包括初始化判断if (slowpath(!cls->isInitialized())):进行一些初始化判断;
  • checkIsKnownClass(cls); 判断当前的class是否已经是注册类
  • realizeAndInitializeIfNeeded_locked:对当前类的rorw进行处理主要是methodlistpropertylist;对superclassmetaclass进行处理;设置isa走位;只要有某一个类进行初始化,与他相关的metaclasssuperclass都会进行初始化;主要作用是为了方法查找过程中对父类的信息进行处理,处理完以上信息为后面的方法查找做准备;
  1. 做完以上准备工作之后正式进入循环查找的流程,这里比较特殊的是,由于循环终止条件的缺失,导致当前循环相当于一个死循环,所以必须依靠终止语句才能跳出循环;循环中主要进行以下操作
  • 再次查找一边缓存curClass->cache.isConstantOptimizedCache(/* strict */true)
  • Method meth = getMethodNoSuper_nolock(curClass, sel);二分查找流程查找curClassmethodlist,此时curClass = self;
  • (curClass = curClass->getSuperclass()) == nil) 二分查找自身未查到时候,执行这里;将curClass指向父类,若父类为空直接forward_imp,否则下面查父类的cahe
  • cache_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的全部流程图如下

慢速查找全流程.png

二分查找细节解析

  1. 二分查找的使用前提是有序数组,这里methodlist中已经将方法进行排序了,数组内部的SEL值随着数组inde的增大而增大,所以我们可以用二分查找;
  2. 作为查找方法的算法,它的性能要求很高;
  3. 查找到方法时需要向前移动,查看前面的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;
}

二分查找原理总结;

  1. 每次查找时,最重要的是确定当前的目标位置目标位置=起始位置+有效数据量的/2
  2. 首次查找起始点为0有效数据量数据总量,
  3. 如果上次的目标位置取出来的数>实际要查找的数,则起始点不变有效数据量变为上次有效数据量的一半,
  4. 如果上次的目标位置取出来的数<实际要查找的数,则起始点变为上次目标位置+1有效数据量变为上次有效数据量-1后的一半

二分查找实战

  1. 第一次二分查找
  • 第一次分析,从0开始,查找数据总数为17,所以目标位置是:0+17/2=8

  • 第一次 base = 0count = 17probe = 0probe = base + count/2 = 0+17/2= 8; list【8】= 17;5 < 17 不做处理;

  1. 第二次分析
  • 0开始,剩下的查找数据总数为8;目标位置是0+8/2=4
  • 第二次 probe = 8base = 0count = 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;
  1. 第三次分析
  • 5开始,剩下的查找数据总数为7/2 =3;目标位置5+3/2=6;
  • 第三次 probe = 4base = 5count = 3probe = base+count/2 = 5+3/2=6list【6】 = 9;5<9不做处理;
  1. 第四次分析
  • 5开始,剩下的查找数据总数为15+1/2=5;最终查找到目标是5
  • 第四次 probe = 6base = 5count = 1;probe = base+count/2=5+1/2=5list【5】= 5,满足条件;