美文网首页
八 OC底层原理 方法查找

八 OC底层原理 方法查找

作者: 可乐冒气 | 来源:发表于2020-12-24 23:15 被阅读0次

    简介

    在前面我们知道,当我们使用xcrun 将文件编译成cpp文件的时候 就可以看到方法的本质就是消息,调用方法也就是发送消息,这就有一个很重要的函数 objc_msgSend, 下面我们就来看看 消息发送的实现

    从下面的注释,我们可以知道,objc_msgSend 就是发送一个消息给类的实例对象
    可以从note 处看到 那么多方法都是基于 objc_msgSend实现的

    /** 
     * Sends a message with a simple return value to an instance of a class.
     * 
     * @param self A pointer to the instance of the class that is to receive the message.
     * @param op The selector of the method that handles the message.
     * @param ... 
     *   A variable argument list containing the arguments to the method.
     * 
     * @return The return value of the method.
     * 
     * @note When it encounters a method call, the compiler generates a call to one of the
     *  functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
     *  Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper; 
     *  other messages are sent using \c objc_msgSend. Methods that have data structures as return values
     *  are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
     */
    OBJC_EXPORT id _Nullable
    objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
        OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
    

    一 方法的快速查找

    1. objc_msgSend 的具体实现

    我们在前面的编译的源码中,全局搜索 objc_msgSend ,在objc-msg-arm64.s 文件中 我们可以看到这样一段代码
    objc_msgSend 函数的是用汇编实现的,

        ENTRY _objc_msgSend // ENTRY 表示函数的入口,是一个格式
        UNWIND _objc_msgSend, NoFrame
    
        cmp p0, #0          // nil check and tagged pointer check
    #if SUPPORT_TAGGED_POINTERS  //在arm64-asm.h 可以看到 当arm64架构时是true
        b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    #else
        b.eq    LReturnZero
    #endif
         // 根据isa, 获取类对象
        ldr p13, [x0]       // p13 = isa
        GetClassFromIsa_p16 p13     // p16 = class
    LGetIsaDone:
        // calls imp or objc_msgSend_uncached -- 调用imp方法实现,或者调用objc_msgSend_uncached
        CacheLookup NORMAL, _objc_msgSend
    #if SUPPORT_TAGGED_POINTERS
    LNilOrTagged:
        b.eq    LReturnZero     // nil check
    
        // tagged
        adrp    x10, _objc_debug_taggedpointer_classes@PAGE
        add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
        ubfx    x11, x0, #60, #4
        ldr x16, [x10, x11, LSL #3]
        adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
        add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
        cmp x10, x16
        b.ne    LGetIsaDone
    
        // ext tagged
        adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
        add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
        ubfx    x11, x0, #52, #8
        ldr x16, [x10, x11, LSL #3]
        b   LGetIsaDone
    // SUPPORT_TAGGED_POINTERS
    #endif
    
    LReturnZero:
        // x0 is already zero
        mov x1, #0
        movi    d0, #0
        movi    d1, #0
        movi    d2, #0
        movi    d3, #0
        ret
    
        END_ENTRY _objc_msgSend
    

    2.CacheLookup 实现

    我们详细看一下 CacheLookup 怎么定义的
    从下面的注释中我们可以看到,这个方法的含义是 从类的缓存中获取方法的imp

    .macro CacheLookup
        //
        // Restart protocol:
        //
        //   As soon as we're past the LLookupStart$1 label we may have loaded
        //   an invalid cache pointer or mask.
        //
        //   When task_restartable_ranges_synchronize() is called,
        //   (or when a signal hits us) before we're past LLookupEnd$1,
        //   then our PC will be reset to LLookupRecover$1 which forcefully
        //   jumps to the cache-miss codepath which have the following
        //   requirements:
        //
        //   GETIMP:
        //     The cache-miss is just returning NULL (setting x0 to 0)
        //
        //   NORMAL and LOOKUP:
        //   - x0 contains the receiver
        //   - x1 contains the selector
        //   - x16 contains the isa
        //   - other registers are set as per calling conventions
        //
    LLookupStart$1:
        // 注释
        //  通过isa 偏移拿到类的方法缓存中的buckets、以及occupied和mask
        // p1 = SEL, p16 = isa
        ldr p11, [x16, #CACHE]              // p11 = mask|buckets
    
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        and p10, p11, #0x0000ffffffffffff   // p10 = buckets
        and p12, p1, p11, LSR #48       // x12 = _cmd & mask
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        and p10, p11, #~0xf         // p10 = buckets
        and p11, p11, #0xf          // p11 = maskShift
        mov p12, #0xffff
        lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
        and p12, p1, p11                // x12 = _cmd & mask
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    
    
        add p12, p10, p12, LSL #(1+PTRSHIFT)
                         // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    // 在 bucket 中查找方法,也就是在类的方法缓存中查找方法,找到就返回
    // 也就是 CacheHit 在上面,可以看到方法实现 ,也就是方法签名,并且调用方法
        ldp p17, p9, [x12]      // {imp, sel} = *bucket
    1:  cmp p9, p1          // if (bucket->sel != _cmd)
        b.ne    2f          //     scan more
        CacheHit $0         // call or return imp
    // 方法没有在缓存中找到,此时调用 CheckMiss 函数,    在CheckMiss函数实现中,又调用了 __objc_msgSend_uncached
    2:  // not hit: p12 = not-hit bucket
        CheckMiss $0            // miss if bucket->sel == 0
        cmp p12, p10        // wrap if bucket == buckets
        b.eq    3f
        ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
        b   1b          // loop
    
    3:  // wrap: p12 = first bucket, w11 = mask
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                        // p12 = buckets + (mask << 1+PTRSHIFT)
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        add p12, p12, p11, LSL #(1+PTRSHIFT)
                        // p12 = buckets + (mask << 1+PTRSHIFT)
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    
        // Clone scanning loop to miss instead of hang when cache is corrupt.
        // The slow path may detect any corruption and halt later.
    
        ldp p17, p9, [x12]      // {imp, sel} = *bucket
    1:  cmp p9, p1          // if (bucket->sel != _cmd)
        b.ne    2f          //     scan more
        CacheHit $0         // call or return imp
        
    2:  // not hit: p12 = not-hit bucket
        CheckMiss $0            // miss if bucket->sel == 0
        cmp p12, p10        // wrap if bucket == buckets
        b.eq    3f
        ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
        b   1b          // loop
    
    LLookupEnd$1:
    LLookupRecover$1:
    3:  // double wrap
        JumpMiss $0
    
    .endmacro
    
    

    这个方法的主体就是查找 cache_t,并且查找cache_t中是否有传进来的方法。 如果缓存中存在就执行 CacheHit,不存在就执行 CheckMiss

    3. CacheHit

    1. 由于之前是normal 查找,所以就直接签名并返回了imp,
    /********************************************************************
     *
     * CacheLookup NORMAL|GETIMP|LOOKUP <function>
     *
     * Locate the implementation for a selector in a class method cache.
     *
     * When this is used in a function that doesn't hold the runtime lock,
     * this represents the critical section that may access dead memory.
     * If the kernel causes one of these functions to go down the recovery
     * path, we pretend the lookup failed by jumping the JumpMiss branch.
     *
     * Takes:
     *   x1 = selector
     *   x16 = class to be searched
     *
     * Kills:
     *   x9,x10,x11,x12, x17
     *
     * On exit: (found) calls or returns IMP
     *                  with x16 = class, x17 = IMP
     *          (not found) jumps to LCacheMiss
     *
     ********************************************************************/
    
    #define NORMAL 0
    #define GETIMP 1
    #define LOOKUP 2
    
    // CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, x16 = isa
    .macro CacheHit
    .if $0 == NORMAL
        TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
    .elseif $0 == GETIMP
        mov p0, p17
        cbz p0, 9f          // don't ptrauth a nil imp
        AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
    9:  ret             // return IMP
    .elseif $0 == LOOKUP
        // No nil check for ptrauth: the caller would crash anyway when they
        // jump to a nil IMP. We don't care if that jump also fails ptrauth.
        AuthAndResignAsIMP x17, x12, x1, x16    // authenticate imp and re-sign as IMP
        ret             // return imp via x17
    .else
    .abort oops
    .endif
    .endmacro
    

    4. CheckMiss

    可知,如果方法没有在缓存中找到,就执行了CheckMiss,之前的类型是 NORMAl,也就执行了 __objc_msgSend_uncached

    .macro CheckMiss
        // miss if bucket->sel == 0
    .if $0 == GETIMP
        cbz p9, LGetImpMiss
    .elseif $0 == NORMAL
        cbz p9, __objc_msgSend_uncached
    .elseif $0 == LOOKUP
        cbz p9, __objc_msgLookup_uncached
    .else
    .abort oops
    .endif
    .endmacro
    

    二 方法的慢速查找

    1. __objc_msgSend_uncached

    全局搜索 ,可以看到这样一段代码, 可以看到函数内部 中有有一段说明 这不是一个C函数方法,可以在类中搜索,也就是MethodTableLookup

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

    2. MethodTableLookup

    可以看到 内部调用了 lookUpImpOrForward 函数, 参数是 obj,sel,class 等

    .macro MethodTableLookup
        
        // push frame
        SignLR
        stp fp, lr, [sp, #-16]!
        mov fp, sp
    
        // save parameter registers: x0..x8, q0..q7
        sub sp, sp, #(10*8 + 8*16)
        stp q0, q1, [sp, #(0*16)]
        stp q2, q3, [sp, #(2*16)]
        stp q4, q5, [sp, #(4*16)]
        stp q6, q7, [sp, #(6*16)]
        stp x0, x1, [sp, #(8*16+0*8)]
        stp x2, x3, [sp, #(8*16+2*8)]
        stp x4, x5, [sp, #(8*16+4*8)]
        stp x6, x7, [sp, #(8*16+6*8)]
        str x8,     [sp, #(8*16+8*8)]
    
        // 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
        
        // restore registers and return
        ldp q0, q1, [sp, #(0*16)]
        ldp q2, q3, [sp, #(2*16)]
        ldp q4, q5, [sp, #(4*16)]
        ldp q6, q7, [sp, #(6*16)]
        ldp x0, x1, [sp, #(8*16+0*8)]
        ldp x2, x3, [sp, #(8*16+2*8)]
        ldp x4, x5, [sp, #(8*16+4*8)]
        ldp x6, x7, [sp, #(8*16+6*8)]
        ldr x8,     [sp, #(8*16+8*8)]
    
        mov sp, fp
        ldp fp, lr, [sp], #16
        AuthenticateLR
    
    .endmacro
    

    3. lookUpImpOrForward

    最终我们在 objc-runtime-new.mm 文件中找到函数实现

    
    /***********************************************************************
    * lookUpImpOrForward.
    * The standard IMP lookup. 
    * Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
    * Without LOOKUP_CACHE: skips optimistic unlocked lookup (but uses cache elsewhere)
    * Most callers should use LOOKUP_INITIALIZE and LOOKUP_CACHE
    * inst is an instance of cls or a subclass thereof, or nil if none is known. 
    *   If cls is an un-initialized metaclass then a non-nil inst is faster.
    * May return _objc_msgForward_impcache. IMPs destined for external use 
    *   must be converted to _objc_msgForward or _objc_msgForward_stret.
    *   If you don't want forwarding at all, use LOOKUP_NIL.
    **********************************************************************/
    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();
    // 判断缓存中是否存在,存在就返回imp
        // Optimistic cache lookup
        if (fastpath(behavior & LOOKUP_CACHE)) {
            imp = cache_getImp(cls, sel);
            if (imp) goto done_nolock;
        }
    
        // runtimeLock is held during isRealized and isInitialized checking
        // to prevent races against concurrent realization.
    
        // runtimeLock is held during method search to make
        // method-lookup + cache-fill atomic with respect to method addition.
        // Otherwise, a category could be added but ignored indefinitely because
        // the cache was re-filled with the old value after the cache flush on
        // behalf of the category.
    
        runtimeLock.lock();
    
        // We don't want people to be able to craft a binary blob that looks like
        // a class but really isn't one and do a CFI attack.
        //
        // To make these harder we want to make sure this is a class that was
        // either built into the binary or legitimately registered through
        // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
        //
        // TODO: this check is quite costly during process startup.
       //判断该类是否是已知类
        checkIsKnownClass(cls);
        // //判断当前类是否加载到内存中(isa和rw信息是否存在)。
        if (slowpath(!cls->isRealized())) {
            //如果不存在进行类的加载,并且会递归加载所有父类、元类。为了确定类的父类、isa链
            cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
            // runtimeLock may have been dropped but is now locked again
        }
        //判断类是否初始化,同样是递归判断所有
        if (slowpath((behavior & LOOKUP_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
        }
    
        runtimeLock.assertLocked();
        curClass = cls;
    
        // The code used to lookpu the class's cache again right after
        // we take the lock but for the vast majority of the cases
        // evidence shows this is a miss most of the time, hence a time loss.
        //
        // The only codepath calling into this without having performed some
        // kind of cache lookup is class_getInstanceMethod().
    // 遍历查询
        for (unsigned attempts = unreasonableClassCount();;) {
            // curClass method list.
    //使用二分法在当前类的方法列表methodList中查找该方法
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp;
                goto done;
            }
            //将当前类切换为父类, 如果当前类的父类是nil,也就是在NSObject中都没有找到则直接跳出循环
            if (slowpath((curClass = curClass->superclass) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                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.
    //在父类的方法缓存中进行查找
            imp = cache_getImp(curClass, sel);
    //如果父类没找到,首次会进入动态方法解析
            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:
        log_and_fill_cache(cls, imp, sel, inst, curClass);
        runtimeLock.unlock();
     done_nolock:
        if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
            return nil;
        }
        return imp;
    }
    
    

    4. 方法最后一次补救机会

    我们可以看到,当系统没有找到imp 实现的时候, 就会跳出for循环,进入这里的动态尝试,这是慢速查找的补救措施,

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
            behavior ^= LOOKUP_RESOLVER;
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
    
    

    这段代码只执行一次,
    第一进入时: behavior=3, LOOKUP_RESOLVER = 2,执行与计算 behavior & LOOKUP_RESOLVER = 2,此时 if 成立,
    然后 behavior 等于 LOOKUP_RESOLVER的反,
    也就是 下一次计算时是 一个数的反 与上这个数,结果一定为0,所以这个这个方法只会走一次
    如果动态解析查找失, 就会 抛出forward_imp异常 ,就crash

    补充 --- 动态方法决议 解析

    resolveMethod_locked

    接下看我们就看看 resolveMethod_locked 函数的执行过程

    static NEVER_INLINE IMP
    resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
    {
        runtimeLock.assertLocked();
        ASSERT(cls->isRealized());
    
        runtimeLock.unlock();
        // 动态方法决议: 重新开始查询
        if (! cls->isMetaClass()) { // 对象查询,类查询
            // try [cls resolveInstanceMethod:sel]
            resolveInstanceMethod(inst, sel, cls);
        } 
        else { // 元类查询
            // try [nonMetaClass resolveClassMethod:sel]
            // and [cls resolveInstanceMethod:sel]
            resolveClassMethod(inst, sel, cls);
            if (!lookUpImpOrNil(inst, sel, cls)) {
                resolveInstanceMethod(inst, sel, cls);
            }
        }
    
        // chances are that calling the resolver have populated the cache
        // so attempt using it
        // 再次调用
        return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
    }
    
    1. 函数入参

    inst: 表示真正持有sel 方法的类(对象方法存储在类中,类方法 存放在 元类中)
    sel: 方法名
    cls: 方法接收的类,也就是调用方法的类
    behavior: 影响方法的调用

    1. 方法解析
    1. 加锁 -> 判定 类是否加载 -> 解锁
    2. 判定类是否是元类,如果不是元类。就代表方法是对象方法,所以进入对象方法查询
    3. 如果是类方法,就调用 resolveClassMethod 查询类方法,然后lookUpImpOrNil 检查是否找到 imp, 如果没有找到就调用 resolveInstanceMethod
      意思就是,如果类方法沿着元类继承链中查找,直到找到 NSObject 元类中都没有找到,接着就是 NSObject本类中,其中存储的是对象方法,所以需要使用resolveInstanceMethod 在查找一次

    resolveInstanceMethod

    static void resolveInstanceMethod(id inst, SEL sel, Class cls)
    {
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        // 获取 resolveInstanceMethod 的方法名 SEL
        SEL resolve_sel = @selector(resolveInstanceMethod:);
        // 检查 类对象的元类中是否有 resolveInstanceMethod 方法,如果没有就 return; 但是在根元类中默认实现了该方法, 所以不会 return
        if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
            // Resolver not implemented.
            return;
        }
        // 消息发送  调用一次 resolveInstanceMethod 方法
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        bool resolved = msg(cls, resolve_sel, sel);
    
        // 再次查询一次 imp, 意思就是  如果上面的 resolveInstanceMethod 方法 实现了 sel, 我们就能拿到 imp,并将其写入 cls 的缓存
        IMP imp = lookUpImpOrNil(inst, sel, cls);
        // 日志收集
        if (resolved  &&  PrintResolving) {
            if (imp) {
                _objc_inform("RESOLVE: method %c[%s %s] "
                             "dynamically resolved to %p", 
                             cls->isMetaClass() ? '+' : '-', 
                             cls->nameForLogging(), sel_getName(sel), imp);
            }
            else {
                // Method resolver didn't add anything?
                _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                             ", but no new implementation of %c[%s %s] was found",
                             cls->nameForLogging(), sel_getName(sel), 
                             cls->isMetaClass() ? '+' : '-', 
                             cls->nameForLogging(), sel_getName(sel));
            }
        }
    }
    

    lookUpImpOrNil

    /* method lookup */
    enum {
        LOOKUP_INITIALIZE = 1,
        LOOKUP_RESOLVER = 2,
        LOOKUP_CACHE = 4,
        LOOKUP_NIL = 8,
    };
    static inline IMP
    lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
    {
       // behavior = 0, LOOKUP_CACHE = 4, LOOKUP_NIL = 8
       return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
    }
    

    内部继续调用了 lookUpImpOrForward ,然后 behavior = 0|4|8 = 12

    此时进入 lookUpImpOrForward 函数中, 以下条件就会成立, 就会在缓存中查找一次
    fastpath(behavior & LOOKUP_CACHE) = 12 & 4 = 4

    
     if (fastpath(behavior & LOOKUP_CACHE)) {
            imp = cache_getImp(cls, sel);
            if (imp) goto done_nolock;
        }
    

    接着 (slowpath(behavior & LOOKUP_RESOLVER)) = 12 & 2 = 0 条件不成立,就不会进入动态决议方法

      if (slowpath(behavior & LOOKUP_RESOLVER)) {
            behavior ^= LOOKUP_RESOLVER;
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
    
    

    所以 lookUpImpOrNil中的 lookUpImpOrForward会循环遍历cls继承链的所有类的cachemethodList来寻找imp

    相关文章

      网友评论

          本文标题:八 OC底层原理 方法查找

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