iOS底层探索之Runtime(三): lookUpImpOrForward慢速查找分析

1. 回顾

iOS底层探索之Runtime(一):运行时&方法的本质 算法

iOS底层探索之Runtime(二): objc_msgSend&汇编快速查找分析编程

在前面的文章中介绍了消息发送(objc_msgSend)流程,主要是汇编快速查找cache的过程,并对汇编源码进行了分析,本章内容主要分析慢速查找_lookUpImpOrForward流程。缓存

2. _lookUpImpOrForward

在汇编的快速查找过程当中若是没有找到缓存,就会进入__objc_msgSend_uncached里面,在__objc_msgSend_uncached 最主要是对MethodTableLookup的处理。markdown

2.1 MethodTableLookup

MethodTableLookup

  • x0寄存器里面存的是imp,并赋值给x17x0是第一个寄存器也是返回值的存储位置,若是impx0里面,必将作一件事情,就是返回,那么结果必定是在bl _lookUpImpOrForward执行后的返回值里面,也就是咱们要找的imp存储的地方,因此接下来的重点就是_lookUpImpOrForward
  • blb是跳转,l是连接寄存器,将下一条指令的地址保存到lr寄存器中,也就是把(mov x17, x0)的指令地址保存在lr中,当_lookUpImpOrForwar执行完之后,执行lr寄存器中的地址。
  • _lookUpImpOrForward找到imp赋值给x17寄存器

_lookUpImpOrForward在源码里面没有找到汇编的实现,由于_lookUpImpOrForward不是汇编写的,是C++写的,因此去掉下划线就能够搜索🔍找到了less

lookUpImpOrForward

lookUpImpOrForward的函数实现里面,确实发现了lookUpImpOrForward返回的是imp,也就有验证了上面👆的汇编分析ide

lookUpImpOrForward返回imp

缓存找不到了,就会进入慢速查找流程,遍历method_list方法列表,遍历是个耗时间的流程,因此就放入了C++中实现,下面重点分析lookUpImpOrForward👇函数

2.2 isKnownClass

lookUpImpOrForward -> checkIsKnownClass(cls) -> checkIsKnownClass -> isKnownClassoop

  • 查询当前的类是否注册到缓存列表中
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学习

  • rwro 进行处理
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();
复制代码
  • 类,元类是否初始化注册

realizeClassWithoutSwift

  • 为何要对类,元类进行初始化呢?

我在iOS底层探索之类的结构(上):ISA文章中已经介绍了isa的走位,和元类的继承关系。当对象调用方法的时候,判断当前类是否初始化,父类、元类是否初始化。目的是,若是当前类中没有实现方法,就去父类查找。若是元类中没有实现类方法,就去根元类查找。递归操做,遍地开花。ui

究竟是怎么递归,怎么循环找方法的呢?请耐心往下看

在这里插入图片描述

  • lookUpImpOrForward -> for循环流程

for循环

个人天哪!开什么玩笑啊?这是循环吗?不要蒙我,我但是学过编程的人啊,循环有三个条件语句的啊!这就一个,后面两个都没有啊!

靓仔,你没有看错,这确实是循环,死循环!

图不重要

这真的是for循环,只是循环体里面,有gotobreak等语句打破死循环

3. 慢速查找分析

3.1 慢速查找流程大纲

  1. 查找本身方法列表Method_list -> sel-imp
  2. 父类中查找 -> 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
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的个数为8count >>= 1就是右移1位,至关于二进制1000,变成0100count =4

  • probe = base + (count >> 1): 就是首地址base加上偏移

  • if (keyValue == probeValue): 两个值相等的时候,if 判断里面的while循环是对分类方法进行处理,类和分类有可能同时实现了相同的方法,probe--就是取分类的方法,由于排好序了,分类方法是排在前面一个位置,最后return &*probe 返回方法的地址。

  • if (keyValue > probeValue): 大于中间值的状况,base = probe + 1,就是4 + 1base等于5count--以后count变为7,进入下一次循环

  • 7右移变为3,完美的避开了4,由于4已经比较过了,这是巧合吗?这就是算法的魅力,我只能说苹果牛逼

苹果牛逼

  • 上面已经执行过一次了,4不符合,那么范围缩小到58区间,那么只能取6或者7进行比较了
  • 循环的话又执行probe = base + (count >> 1)base上面算过了等于5count等于3,再count >> 1以后count = 1probe就等于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使用二分查找算法,提升查找效率

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 以为学习到了的,能够来一波,收藏+关注,评论 + 转发,以避免你下次找不到我😁🌹

🌹欢迎你们留言交流,批评指正,互相学习😁,提高自我🌹

相关文章
相关标签/搜索