美文网首页底层原理
objc_msgSend 消息转发流程探究一

objc_msgSend 消息转发流程探究一

作者: 晨曦的简书 | 来源:发表于2021-06-27 16:34 被阅读0次

    运行时理解


    通过这张图片我们可以看到,我们平时调用 oc 方法其实本质就是调用 runtimeapi,就是发消息。那么我们平常的 oc 方法调用,在底层又是如何实现的呢?
    #import <Foundation/Foundation.h>
    #import <objc/message.h>
    
    @interface CXPerson : NSObject
    - (void)sayHello;
    @end
    
    @implementation CXPerson
    - (void)sayHello {
        NSLog(@"CXPerson - sayHello");
    }
    @end
    
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            CXPerson *person = [CXPerson alloc];
            [person sayHello];
        }
        return 0;
    }
    

    我们先建立一个工程,在 main.m 文件定义一个 CXPerson 类,并调用 sayHello 方法。

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            CXPerson *person = ((CXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CXPerson"), sel_registerName("alloc"));
            ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
        }
        return 0;
    }
    

    然后打开终端,cd 到 main.m 所在文件目录,然后输入 clang -rewrite-objc main.m -o main.cpp 命令,最后会看到生成了一个 main.cpp 文件。 然后找到 main 函数的实现代码。可以看到 allocsayHello 方法的调用在底层都会转为 objc_msgSend 的调用形式。

    通过代码可以看到消息的发送需要两个参数 objc_msgSend(消息接受者, 消息的主题(sel + 参数)),消息接收者跟消息主题。那么我们是否能直接调用底层的消息发送方法呢?这里我们来试一下。


    这里可以看到,我们自己直接调用 objc_msgSend 方法也是可以的。

    objc_msgSend 源码探究


    这里我们来看一下 objc_msgSend 方法再源码中的实现,全局搜索 objc_msgSend 我们可以看到,不同架构下 objc_msgSend 对应的汇编代码。因为我们真机常用的是 arm64 架构,这里我们就分析一下 arm64 架构下的汇编实现。
    1. ENTRY _objc_msgSend (进入到 objc_msgSend )
        ENTRY _objc_msgSend
        UNWIND _objc_msgSend, NoFrame
    
    // p0代表消息接受者的地址,这里判断消息接收者是否存在
        cmp p0, #0          // nil check and tagged pointer check
    
    // 如果消息接受者不存在,就会走到这里。这这里也会有判断
    #if SUPPORT_TAGGED_POINTERS
    // 如果为SUPPORT_TAGGED_POINTERS类型走到这里
        b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    #else
    // 不为SUPPORT_TAGGED_POINTERS类型走到这里
        b.eq    LReturnZero
    
    // 如果消息接收者存在就会走到这里
    #endif
        ldr p13, [x0]       // p13 = isa (消息接收者的首地址)
        GetClassFromIsa_p16 p13, 1, x0  // p16 = class
    // 这个通过消息接收者 receviece 获取 class 的原因是因为方法存在 cache 里面,而 cache 是类结构体的属性。cache 中有 cache_getIMP方法。通过 sel 获取 imp。
    LGetIsaDone:
        // calls imp or objc_msgSend_uncached
        CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
    
    #if SUPPORT_TAGGED_POINTERS
    LNilOrTagged:
        b.eq    LReturnZero     // nil check
        GetTaggedClass
        b   LGetIsaDone
    // SUPPORT_TAGGED_POINTERS
    #endif
    

    这一步主要是判断消息接受者是否存在,如果存在的话就把 isa 赋值给 p13,并作为参数带入到 GetClassFromIsa_p16 方法。

    1. .macro GetClassFromIsa_p16 src, needs_auth, auth_address (通过 isa 获取 class)
    .macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
    
    #if SUPPORT_INDEXED_ISA
        // Indexed isa
        mov p16, \src           // optimistically set dst = src
        tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
        // isa in p16 is indexed
        adrp    x10, _objc_indexed_classes@PAGE
        add x10, x10, _objc_indexed_classes@PAGEOFF
        ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
        ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
    1:
    
    #elif __LP64__
    .if \needs_auth == 0 // _cache_getImp takes an authed class already
        mov p16, \src
    .else
        // 64-bit packed isa
            // and    $0, $1, #ISA_MASK $1就是 p13,就是 isa,与上ISA_MASK得到 class,并赋值给 p16
        ExtractISA p16, \src, \auth_address
    .endif
    #else
        // 32-bit raw isa
        mov p16, \src
    
    #endif
    

    这一步主要是通过 isa 获取 class,因为 cache 是类结构体的一个属性。而 chache_getIMP 存在于 cache 中。


    全局搜索可以看到 ExtractISA 的实现,代表 $1 与上 ISA_MASK 并赋值给 $0
    1. .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    
    
    // 把 `p16` 移动到 `x15`
        mov x15, x16            // stash the original isa
    
    
    // 开始进入objc_msgSend流程
    LLookupStart\Function:
        // p1 = SEL, p16 = isa
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
        ldr p10, [x16, #CACHE]              // p10 = mask|buckets
        lsr p11, p10, #48           // p11 = mask
        and p10, p10, #0xffffffffffff   // p10 = buckets
        and w12, w1, w11            // x12 = _cmd & mask
    
    
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // 如果是 arm64 结果会走到这里
        ldr p11, [x16, #CACHE]          // p11 = mask|buckets
    
    
        #if CONFIG_USE_PREOPT_CACHES
        // 全局搜索CONFIG_USE_PREOPT_CACHES(#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST#define CONFIG_USE_PREOPT_CACHES 1) 得到CONFIG_USE_PREOPT_CACHES等于 1,所以走到这里
          #if __has_feature(ptrauth_calls)
        tbnz    p11, #0, LLookupPreopt\Function
        and p10, p11, #0x0000ffffffffffff   // p10 = buckets
          #else
    
    
        //p11` 与上 `0x0000fffffffffffe` 得到 `buckets` 并赋值给 `p10
        and p10, p11, #0x0000fffffffffffe   // p10 = buckets
        //p11跟 0 相比较,如果不为零就跳转到到 LLookupPreopt
        tbnz    p11, #0, LLookupPreopt\Function
    
          #endif
    // 如果为零会走到这里。p1 右移7 个位置赋值给 x12
        eor p12, p1, p1, LSR #7
        and p12, p12, p11, LSR #48      // x12 = (_cmd ^ (_cmd >> 7)) & mask
        #else
    
    // 如果模拟器会走到这里,p11为 cache,与上0x0000ffffffffffff得到 buckets 并赋值给 p10
        and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    //p11右移动 48 位就会得到 mask 的值
    // p1(_cmd) & mask -> index -> p12 = index
        and p12, p1, p11, LSR #48       // x12 = _cmd & mask
    
    #endif // CONFIG_USE_PREOPT_CACHES
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        ldr p11, [x16, #CACHE]              // p11 = mask|buckets
        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
    
    
    // p11 cache -> p10 = buckets
    // p1(_cmd) & mask = index -> p12
    // 全局搜索 PTRSHIFT 等于 3 #define PTRSHIFT 3  
    // (_cmd & mask) << (1+PTRSHIFT) ->  (_cmd & mask) << 4
    // buckets + 内存平移(1,2,3,4) = b[i]
    // p13 当前查找的 bucket
        add p13, p10, p12, LSL #(1+PTRSHIFT)
                                                    // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    
    
                            // do {
    // * bucket-- p17, p9同时存入bucket--
    // 拿到相应的bucket里面的东西 imp(p17)  sel(p9)
    // 查到的 sel 跟我们要查的 sel 进行比较,如果一样,就会走到 2 里面,不等于就会走到 3 里面,其实就是循环查找
    1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
        cmp p9, p1              //     if (sel != _cmd) {
        b.ne    3f              //         scan more
                            //     } else {
    2:  CacheHit \Mode              // hit:    call or return imp
                            //     }
    3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
        cmp p13, p10            // } while (bucket >= buckets)
        b.hs    1b
    

    调用 CacheLookup 方法并带入 NORMAL, _objc_msgSend, __objc_msgSend_uncached 这几个参数。

    1. ldr p11, [x16, #CACHE]
      x16 的地址平移 CACHE 大小。
    2. #define CACHE (2 * __SIZEOF_POINTER__)
      全局搜索 #define CACHE 可以得到 CACHE 是两个指针的大小为 16个字节。
    3. [x16, #CACHE] -> p11 -> cache_t
      所以 p11 就是等于 cache_t
    4. CONFIG_USE_PREOPT_CACHES
    #if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
    #define CONFIG_USE_PREOPT_CACHES 1
    

    全局搜索 CONFIG_USE_PREOPT_CACHES,得到 CONFIG_USE_PREOPT_CACHES 等于 1。

    //p11` 与上 `0x0000fffffffffffe` 得到 `buckets` 并赋值给 `p10
    and p10, p11, #0x0000fffffffffffe   // p10 = buckets
    //p11跟 0 相比较,如果不为零就跳转到到 LLookupPreopt
    tbnz    p11, #0, LLookupPreopt\Function
    
    1. LLookupPreopt\Function

    2. CacheHit

    .macro CacheHit
    .if $0 == NORMAL
    // 如果 $0 等于 NORMAL就会走到TailCallCachedImp
        TailCallCachedImp x17, x10, x1, x16 // authenticate and 
    
    1. TailCallCachedImp
    //CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
    .macro TailCallCachedImp
        // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
    // $0(x17) ^  $3(isa) = 编码(imp)
        eor $0, $0, $3
    // 跳转到$0(imp),就是执行 imp 方法
        br  $0
    

    最后总结

    objc_msgSend(recevier, _cmd) sel -> imp

    1. 判断 recevier 是否存在
    2. 通过 recevier 找到 isa 找到 class(p16)
    3. class 内存平移得到 cache(bucket mask)
    4. bucket掩码 -> bucket
    5. mask掩码 -> mask
    6. insert哈希函数(mask_t)(value & mask);
    7. 第一次查找 index
    8. bucket + index -> 整个缓存里的第几个 bucket
    9. bucket{imp, sel}
    10. sel 与我们查找的 _cmd比较,如果相等 -> cacheHit(缓存命中) -> imp ^ class(isa) = imp -> (br)调用 imp
    11. 如果不相等 bucket-- ->再次平移 -> 找到 bucket 再次比较
    12. 死循环 遍历查找
    13. 一直找不到就会走的 __objc_msgSend_uncached(执行CacheLookup方法带入的第三个参数)

    未完待续。。。

    相关文章

      网友评论

        本文标题:objc_msgSend 消息转发流程探究一

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