美文网首页
iOS底层探索--方法慢速查找

iOS底层探索--方法慢速查找

作者: spyn_n | 来源:发表于2021-07-01 14:40 被阅读0次

前言

  在 iOS底层探索--方法快速查找(汇编)流程&慢速查找初探 这一篇章我们研究了方法的快速查找流程,以及慢速方法查找初探:如果在 cache 找不到 imp ,就会调用 MissLabelDynamic 参数,实际调用了 _objc_msgSend_uncached 方法,然后再该方法中调用MethodTableLookup函数,在 MethodTableLookup 中调用了_lookUpImpOrForward 函数,我们研究过程就成功的从汇编到了C/C++慢速查找过程。

转换过程

源码中查找lookUpImpOrForward

  在源码中查找一下lookUpImpOrForward方法调用及查找流程发现有几个地方调用了lookUpImpOrForward,分别是:

  1. 在获取类的class_getClassMethod和类的实例方法class_getInstanceMethod时,即调用runtime API获取类方法和实例方法时
  2. _lookUpImpTryCache处调用
lookUpImpOrForward调用

lookUpImpOrForward流程

  1. checkIsKnownClass(cls) 检测类是否是合法注册的类
  2. realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE)如果尚未实现,则实现给定的类;如果尚未初始化,则初始化该类(这部分个人认为对于懒加载类,需要时才加载,根据类内存,绑定rorwrwesuperclassisacategoriesprotocols,对方法根据地址进行排序等操作)
  3. for死循环递归查找
      1. getMethodNoSuper_nolock(curClass, sel)查找Method
      2. 找到就log_and_fill_cache(cls, imp, sel, inst, curClass);
      3. 找不到,则获取父类 curClass->getSuperclass()
      4. 在superclass的cache中查找 imp = cache_getImp(curClass, sel),此时进入汇编快速查找流程,如果找到则同步本类的cache
        • _cache_getImp
        • CacheLookup方法
      5. 继续循环1、2、3、4直到curClass == nil结束循环
  4. 循环递归找不到方法,则imp = _objc_msgForward_impcachebreak跳出循环,并尝试进行方法解析resolveMethod_locked (inst , sel , cls , behavior),保证只进行解析一次(如何做到的呢?)
    • behavior = 3 ,LOOKUP_RESOLVER = 2 if (slowpath(behavior & LOOKUP_RESOLVER)){ behavior ^= LOOKUP_RESOLVER}
      1. 第一次来 3 & 2 = 2 ,条件成了,进入 然后 behavior = 3 ^ 2 = 1
      2. 第二次来 1 & 2 = 0 ,条件不成立,不进入
        • 非元类 !cls->isMetaClass()
          • resolveInstanceMethod(inst, sel, cls);
        • 元类
          • resolveClassMethod(inst, sel, cls);
  5. 如果以上能找到IMP,则打印并填充缓存log_and_fill_cache(cls, imp, sel, inst, curClass);
  6. 如果找不到imp = _objc_msgForward_impcache
lookUpImp慢速查找流程 image.png
getMethodNoSuper_nolock流程

  该方法中可以看到,方法存储的结构是一个二维数组,会遍历调用调用search_method_list_inline,该方法会根据method列表是否修复过(进行排序过,这部分是在类的加载的时候做的事情),调用findMethodInSortedMethodListfindMethodInUnsortedMethodList。排序过的方法列表findMethodInSortedMethodList函数会进行而为查找算法查找,对未排序的方法列表,findMethodInUnsortedMethodList函数进行挨个遍历查找。

getMethodNoSuper_nolock流程

方法的查找(递归算法查找)

template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInUnsortedMethodList(SEL sel, const method_list_t *list, const getNameFunc &getName)
{
    // 递归遍历未排序方法列表
    for (auto& meth : *list) {
        if (getName(meth) == sel) return &meth;
    }
    return nil; // 找不到则返回nil
}

方法的查找(二分查找算法)

/***********************************************************************
 * search_method_list_inline
 **********************************************************************/
template<class getNameFunc>
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; // 探针(一个来回摆动的指针,不断的取来获取name与sel对比)又因为SEL是进行hash之后的,保证了其唯一性

    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))) {
                // 如果前一个的名字sel也一样,并且探针不是起始位置,则表示前一个是分类中的方法,说明了如果分类重写了类的方法,调用该方法的时候会调用分类的方法
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) { // 如果探针的位置小于keyValue,说明在后半段(base从探针+1的位置开始),否则在前半段(base不变)
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
二分查找案例1
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                // 如果前一个的名字sel也一样,并且探针不是起始位置,则表示前一个是分类中的方法,说明了如果分类重写了类的方法,调用该方法的时候会调用分类的方法
                probe--;
   }

  如果找到了方法,则循环遍历取探针的前一个方法,直到取到最前面一个相同的SEL。这可以证明如果分类重写了原来类的方法,则会调用分类方法。在底层中方法并不是覆盖了,只是插入方法列表的时候放到了原类的方法前面。

二分查找案例2

cache的insert流程:
cls->cache.insert(sel, imp, receiver);

  1. 检测类的初始化标志位,如果没有初始化就什么不干
  2. 拿到newOccupied,oldCapacity,capacity初始值=oldCapacity
  3. 判断是否第一次申请缓存空间还是需要扩容
    1. 判断如果是空缓存,如果是就申请缓存空间reallocate(oldCapacity, capacity, /* freeOld */false); 创建容量为capacity大小(INIT_CACHE_SIZE,即是1<<2 = 4)的空间
      • 获取旧的buckets(根据第三个参数为true就进行释放旧值)
      • 调用allocateBuckets(newCapacity);创建一个capacity(4个)大小容量的buckets容器(以下说明创建的时候最后一个bucket_t就已经被占用了作为一个标记)
        • calloc(bytesForCapacity(newCapacity), 1);创建容器
        • 获取到最后一个bucket_t指针end
        • 设置最后一个bucket_t的SEL为1,IMP指向容器的首地址,也就是第一个bucket_t
        • 检测环境变量OBJC_PRINT_CACHE_SETUP为true,打印缓存容器
      • setBucketsAndMask(newBuckets, newCapacity - 1);设置的时候排除最后一个结束标志位固定占用的大小
        • _occupied初始化为0
      • 如果是扩容也就是freeOld=true则collect_free(oldBuckets, oldCapacity)
    2. 如果有缓存空间了,新的占用量newOccupied + 1个结束标记bucket <= 缓存填充比例3/4.
    3. 如果达到扩容条件(也就是大于缓存填充比例),就进行2倍扩容,但是capacity最大不超过2^16=65536,重新调用reallocate(oldCapacity, capacity, true);创建更大的容器,并且把旧的bucktes释放,也就是把所有的bucket_t丢弃。(为什么丢弃:因为将旧数据挪到新的buckets的CPU开销成本太大)
  4. 创建一个新的bucket_t b = buckets(), 对sel进行hash ---> (sel & (capacity-1))得到一个下标
  5. 进行do...while循环,查找一个未使用的插槽并插入
    • 以sel的hash值为key,从bucket中取出sel,
      • 如果不存在_occupied++,给bucket set sel和IMP set(b,sel, imp, cls()),设置的时候根据是都原子和encode对IMP进行编码再存储
      • 如果sel已经存在,说明已存在缓存,则什么也不干直接退出
    • 条件cache_next(i, (capacity - 1)) --> (i + 1) & (capacity - 1) != begin也就是进行在哈希
  6. 如果插入不成功,就bad_cache
cache的insert流程

相关文章

网友评论

      本文标题:iOS底层探索--方法慢速查找

      本文链接:https://www.haomeiwen.com/subject/dhwhultx.html