美文网首页
_lookUpImpOrForward慢速方法查找

_lookUpImpOrForward慢速方法查找

作者: 冼同学 | 来源:发表于2021-07-10 11:22 被阅读0次

    前言

    《IOS底层原理之Runimte 运行时&方法的本质》一文中已经分析了objc_msgSend查找缓存(cache)的流程,也就是objc_msgSend的快速查找流程,当cache中找不到imp的时候,就会进入慢速查找的过程。

    资料准备

    objc_msgSend_uncached流程实现

    当在cache中没有找到目标方法时候,汇编会进入MissLabelDynamic流程,而MissLabelDynamic = __objc_msgSend_uncached,那么我们在源码中直接搜索__objc_msgSend_uncached,得到一下的实现代码:

        STATIC_ENTRY __objc_msgSend_uncached
        UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    
        // THIS IS NOT A CALLABLE C FUNCTION
        // Out-of-band p15 is the class to search
    
        MethodTableLookup
        TailCallFunctionPointer x17
    
        END_ENTRY __objc_msgSend_uncached
    

    以上就是短短的几行汇编代码,那么他做的是什么呢,主要是看MethodTableLookupTailCallFunctionPointer方法的实现。x17又是什么呢?以下查看TailCallFunctionPointer方法的实现。(源码中全局搜索TailCallFunctionPointer)

    TailCallFunctionPointer方法实现

        // A12 以上 iPhone X 以上的
        #if __has_feature(ptrauth_calls)
        ...
        #else
        ...
        .macro TailCallFunctionPointer
                // $0 = function pointer value
                br  $0
        .endmacro
        ...
        #endif
    

    通过TailCallFunctionPointer的汇编实现流程并没有发现x17寄存器赋值的地方,那么猜想在MethodTableLookup方法中。(其实看MethodTableLookup方法名字就猜得出是干嘛的 --> 查询方法表

    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              //x16 = class赋值给了x2
        mov x3, #3               //x3 = 3
        bl  _lookUpImpOrForward  //跳转到_lookUpImpOrForward(objc_msgSend的慢速方法查找入口)
                                 //bl跳转时候会保存吓一跳指令的位置到lr寄存器中,保存回家的路
    
        // IMP in x0
        mov x17, x0              //x0寄存器作为返回值的存储地方,保存的是_lookUpImpOrForward方法返回的imp
                                 //那么x17 = imp
    
        RESTORE_REGS MSGSEND
    
    .endmacro
    

    MethodTableLookup方法内部实现主要是跳转到_lookUpImpOrForward方法(objc_msgSend的慢速查找流程),返回查找到的impx17
    注意:汇编中的_lookUpImpOrForward方法前面带下划线_,那么代表的是跳转到C++的方法中。

    疑问:为什么_lookUpImpOrForward实现用的是C++,而之前说道查找cache的时候用的是汇编呢?

    • 汇编更接近机器语言,查询速度更快。缓存查找流程是快速在缓存中找到方法,而慢速查找流程是不断的遍历methodlist过程,这个过程很慢
    • 方法中有的参数是不确定的,但是在C语言中参数必须是确定的,而汇编可以让你感觉更加的动态化

    objc_msgSend_uncached总结

    • 通过MethodTableLookup查询将查询到imp作为返回值存在x0寄存器,将x0寄存器的值赋值给x17寄存器
    • 通过TailCallFunctionPointer x17直接调用x17寄存器中的imp,找到方法体并实现。
      -__objc_msgSend_uncached--> MethodTableLookup--> _lookUpImpOrForward -->TailCallFunctionPointer-->方法实现了。

    objc_msgSend_uncached流程图

    objc_msgSend_uncached流程

    lookUpImpOrForward

    lookUpImpOrForward方法是通过传进来的SEL来查找IMP的,那么具体怎样查询,请看看lookUpImpOrForward方法流程:

    IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
    {
        //定义消息转发forward_imp  //behavior传进来的是3,代表LOOKUP_INITALIZE\LOOKUP_REDOLVER
        const IMP forward_imp = (IMP)_objc_msgForward_impcache;
        //返回的imp
        IMP imp = nil;
         //当前查找的cls
        Class curClass;
    
        runtimeLock.assertUnlocked();
        /*
         判断类是否已经初始化,没有初始化:behavior = LOOKUP_INITALIZE\LOOKUP_REDOLVER\LOOKUP_NOCACHE
         发送类的第一条消息通常是+new或者+alloc或者+self的初始化
         */
        if (slowpath(!cls->isInitialized())) {
           //修改behavior的状态
            behavior |= LOOKUP_NOCACHE;
        }
         //运行时上锁,防止多线程访问出现错乱的情况
        runtimeLock.lock();  
        checkIsKnownClass(cls);  //是否注册类-->是否被dyld加载的类,注册后的类会加入allocatedClasses表
        //实现初始化过程中需要的类-->初始化类和父类,为以后的查找做好准备。
        cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
        // runtimeLock may have been dropped but is now locked again
        runtimeLock.assertLocked();
        //赋值需要查找的类
        curClass = cls;
    
        //这是一个死循环,除非进入goto方法(return),或者break
        //在获取锁后,代码会再次查找类的缓存,在大多数情况下是不是命中的,因此浪费时间。
        //唯一没有执行某种缓存查找的路径就是class_getInstanceMethod()
        
        for (unsigned attempts = unreasonableClassCount();;) {
            //判断是否有共享缓存,一般指的是系统方法,如:NSLog等,自定义的方法一般不走
            if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    #if CONFIG_USE_PREOPT_CACHES
                /*
                 再次查询共享缓存,因为在比查询的过成功其他线程调用了某个系统方法;
                 因此共享缓存有机会存在目标方法
                 */
                //缓存中根据sel查找imp
                imp = cache_getImp(curClass, sel);
                //如果查到目标imp,就跳到done_unlock,返回imp
                if (imp) goto done_unlock;
                curClass = curClass->cache.preoptFallbackClass();
    #endif
            } else {
                // curClass method list.
                //在curClass中,使用二分法查找methodList
                Method meth = getMethodNoSuper_nolock(curClass, sel);
                //如果找到了sel对应的方法
                if (meth) {
                    //提出方法的imp
                    imp = meth->imp(false);
                    //跳转到done流程
                    goto done;
                }
          ........
         //此处省略了一部分代码(主要是查找父类,进行以上几步的操作)
    // No implementation found. Try method resolver once.
        //动态方法决议
        if (slowpath(behavior & LOOKUP_RESOLVER)) {
            //behavior = 3   LOOKUP_RESOLVER = 2
            //3^2 = 1
            behavior ^= LOOKUP_RESOLVER;
            //动态方法决议
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
    
     done:
        //behavior =3  LOOKUP_NOCACHE = 8
        //3 & 8 = 0b0011 & 0b1000 = 0b000 = 0,以下的判断成立
        if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    #if CONFIG_USE_PREOPT_CACHES
            while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
                cls = cls->cache.preoptFallbackClass();
            }
    #endif
            //将查询到sel和imp插入到当前类的缓存
            log_and_fill_cache(cls, imp, sel, inst, curClass);
        }
     done_unlock:
        //runtime解锁
        runtimeLock.unlock();
        /*
         如果behavior & LOOKUP_NIL成立的话,behavior != LOOKUP_NIL
         且imp == forword_imp,没有查询到直接返回nil
         */
        if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
            return nil;
        }
        //返回查到的imp
        return imp;
    }
    

    分析得出lookUpImpOrForward大致以下的流程:

    • 慢速查找流程
      • 判断cls(类)是否已经初始化,改变behavior的值。
      • 判断cls(类)是否已经注册,没有注册的话直接报错
      • 是否实现cls(类),如果没有实现就会根据isa的走位链继承链去初始化其父类(super class)元类(metal class),以方便之后的查找。
    • cls遍历查询
      • 判断是否有共享缓存,目的是有可能在查过过程中这个方法被调用缓存了,如果有的话直接从缓存中取,没有共享缓存则开始到本类中查询。
      • 在类中采用二分查找算法查找methodlist中的方法,如果找到插入缓存中,循环结束。
    • 父类缓存中查询
      • 此时curClass = superclass,到父类的缓存中找,如果找到则插入到本类缓存中。如果父类中返回的是forward_imp跳出遍历,执行消息转发
      • 如果没有在superclass的缓存中找到,那么curClass = superclass就会进入像上面cls循环查找methodlist的流程,直到curClass = nilimp = forword_imp就进行消息的转发
    • 动态方法决议
      • 如果cls以及父类都没有查询到,此时系统会给你一次机会,判断是否执行过动态方法决议,如果没有则走动态方法决议。(下一篇文章详细分析哦)。
      • 如果动态决议方法执行过,imp = forward_imp会走done流程插入缓存,会走done_unlock流程 return imp进入消息转发阶段。

    behavior

    /* method lookup */
    enum {
        LOOKUP_INITIALIZE = 1,
        LOOKUP_RESOLVER = 2,
        LOOKUP_NIL = 4,
        LOOKUP_NOCACHE = 8,
    };
    
    • LOOKUP_INITIALIZE: 控制是否去进行类的初始化。有值初始化,没有不初始化。
    • LOOKUP_RESOLVER:是否进行动态方法决议。有值决议,没有值不决议。
      -LOOKUP_NIL:是否进行forward(消息转发)。有值不进行,没有值进行。
    • LOOKUP_NOCACHE:是否插入缓存。有值不插入缓存,没有值插入。

    lookUpImpOrForward 流程图

    lookUpImpOrForward 流程图

    realizeAndInitializeIfNeeded_locked -- 实现和实例化类

    static Class
    realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
    {
        runtimeLock.assertLocked();
        //经验判断cls->isRealized()是小概率时事件,cls->isRealized()大概率是=YES
        //判断类是否实现,目的是实现isa走位图的isa走位链与继承链
        if (slowpath(!cls->isRealized())) {
            cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
            // runtimeLock may have been dropped but is now locked again
        }
        //判断类是否已经初始化,没有的话进入判断实现初始化
        if (slowpath(initialize && !cls->isInitialized())) {
            cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
            // runtimeLock may have been dropped but is now locked again
    
            // If sel == initialize, class_initialize will send +initialize and
            // then the messenger will send +initialize again after this
            // procedure finishes. Of course, if this is not being called
            // from the messenger then it won't happen. 2778172
        }
        return cls;
    }
    
    • realizeClassMaybeSwiftAndLeaveLocked方法中的realizeClassWithoutSwift就是去实现类的isa走位链继承链中相关的
    • initializeAndMaybeRelock的initializeNonMetaClass就是初始化父类的。

    findMethodInSortedMethodList -- 二分法查找算法

    ALWAYS_INLINE static method_t *
    findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
    {
        ASSERT(list);
    
        auto first = list->begin();   //第一个method的位置
        auto base = first;            //base = 第一个method的位置
        decltype(first) probe;        //中间值,拿来进行以下二分法运算的
    
        uintptr_t keyValue = (uintptr_t)key; //把key转化成uintptr_t类型,因为修复过后的method_list_t中的元素进行了排序
        uint32_t count;   //方法数组的个数
        /*
         count >>= 1 = count = count >> 1 =count = count/2(二分)
         假如:count = list->count = 8
         下一个循环的时候count >>= 1 = 7 >> 1 = 3;
         */
        
        for (count = list->count; count != 0; ) {
            probe = base + (count >> 1);  //第一次: probe = 0+8>>1 = 4
            //获取中间值的sel(方法编号)
            uintptr_t probeValue = (uintptr_t)getName(probe);
            //判断与要查找的sel是否匹配,匹配成功进入以下判断
            if (keyValue == probeValue) {
                // `probe` is a match.
                // Rewind looking for the *first* occurrence of this value.
                // This is required for correct category overrides.
                //查找分类中同名的sel,如果匹配了就取分类中的方法,多个分类的话要看编译的顺序
                while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                    probe--;
                }
                //返回方法地址
                return &*probe;
            }
            //没有匹配上,大于的情况下,
            if (keyValue > probeValue) {
                base = probe + 1;   //base = 5,base 的开始位置为5,因为4已经比较过了,没必要了
                count--;    //count = 8 - 1 = 7,为了减少循环对比,以为count = 8的是否在第一轮已经比较过了
            }
        }
        //查询没有的话返回nil
        return nil;
    }
    

    注意:方法列表中的方法是经过修复的,意思就是按照sel大小进行过排序的。

    • 二分法查找算法其实就是每次找到范围内的·中间位置keyValue比较,如果相等直接返回查找到的方法(如果有分类方法就返回分类方法)
    • 如果不相等则继续二分法查询,不断缩小查询的范围,如果最后还是没有查询到则返回nil(count-- 目的就是过滤之前重复的判断)

    二分法查找流程

    如果上面代码看得比较不清晰的话,那么下面的流程如会给你梳理一遍,请看下图:

    二分法流程(大于中间值)
    二分法流程(小于中间值)
    不难发现keyValue 大于 probeValue算法和keyValue 小于 probeValuecount的利用有一点区别,可以细细体会count的设计,小于的算法 count是作为上边界(就是我们平时理解的二分法),大于的算法,count的设计就很巧妙,不在作为上边界,主要看probe就可以了(排除了重复的循环,增强了效率)。

    cache_getImp -- 快速查找

    通过源码的查找,发现cache_getImp使用汇编语言实现了,代码如下:

        STATIC_ENTRY _cache_getImp
    
        GetClassFromIsa_p16 p0, 0
        CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
    
    LGetImpMissDynamic:
        mov p0, #0
        ret
    
    LGetImpMissConstant:
        mov p0, p2
        ret
    
        END_ENTRY _cache_getImp
    
    • GetClassFromIsa_p16宏定义和我们开始在本类中查询缓存方法一样,但是参数不一样。
    • 最终也是调用CacheLookup方法来进行查找。

    GetClassFromIsa_p16

    《IOS底层原理之Runimte 运行时&方法的本质》一文中已经详细的分析了GetClassFromIsa_p16的实现流程,这里就简单的回顾一下:

    .macro GetClassFromIsa_p16 src, needs_auth, auth_address 
    /* note: auth_address is not required if !needs_auth */
    
    #if SUPPORT_INDEXED_ISA  // armv7k or arm64_32
        ...//省略
    1:
    
    #elif __LP64__
    .if \needs_auth == 0 // _cache_getImp takes an authed class already
        mov p16, \src
    .else //needs_auth = 1 所以走下面的流程
        // 64-bit packed isa
            //把 \src 和 \auth_address 传进ExtractISA 得到的结果赋值给p16寄存器
        ExtractISA p16, \src, \auth_address  
    .endif
    #else
        // 32-bit raw isa
        mov p16, \src     //返回p16 = class
    
    #endif
    

    cacheLookup过程已经在上一文进行了详细的分析,这里就不详细分析。

    • 如果缓存没有命中走LGetImpMissDynamic流程。
    • 如果缓存命中Mode = GETIMP
    LGetImpMissDynamic:
        mov p0, #0
        ret
    
    • 在缓存中没有找到所需方法的时候会令p0 = 0imp = nil
    • 至于缓存命中的过程就不仔这里详细分析了,上一文中已经详细分析了。
      注意:缓存中获取imp是编码过的,此时imp ^ class =解码后的imp
    .macro AuthAndResignAsIMP
        // $0 = cache imp  , $1 = buckets的地址, $2 = SEL  $3 = class
            // $0 = $0 ^ $3 = imp ^ class = 解码后的imp  
        eor $0, $0, $3
    .endmacro
    

    实例查找

    我们创建一个XXPerson类的实例,定义一个没有方法体的方法sayLost,然后分析崩溃之前的流程,请往下看:


    出现了unrecognized崩溃信息,那么这个信息是在哪里发出来的呢?[perosn sayLost]也走了快速查找流程慢速查找流程动态方法决议,最后消息转发,最后还是没找到报unrecognized,全局搜索doesNotRecognizeSelector或者unrecognized selector sent to instance,在源码中搜索得出:
    // Replaced by CF (throws an NSException)
    + (void)doesNotRecognizeSelector:(SEL)sel {
        _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                    class_getName(self), sel_getName(sel), self);
    }
    
    // Replaced by CF (throws an NSException)
    - (void)doesNotRecognizeSelector:(SEL)sel {
        _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                    object_getClassName(self), sel_getName(sel), self);
    }
    
    __attribute__((noreturn, cold)) void
    objc_defaultForwardHandler(id self, SEL sel)
    {
        _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                    "(no message forward handler is installed)", 
                    class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                    object_getClassName(self), sel_getName(sel), self);
    }
    

    doesNotRecognizeSelector方法是怎么样走下去的,这个我们下篇文章会进行详细的分解。详细分析消息的转发流程。

    补充

    创建NSObject+XL分类,在分类中添加sayHello方法,并在分类中实现sayHello方法。然后进行如下得调用:


    类调用对象方法调用成功了原因是什么?在oc底层没有所谓的实例方法和类方法,获取一个类方法实际上就是获取元类的实例方法,没找到找到根源类,根源类也没有,最后找到NSObject所以可以找到sayHello方法。

    总结

    和谐学习,不急不躁!知识使人快乐!!

    相关文章

      网友评论

          本文标题:_lookUpImpOrForward慢速方法查找

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