[iOS底层]-objc_msgSend快速查找原理

2020/9/23 posted in  iOS底层 总阅读量

方法是如何读取的?

[iOS底层]-cache_t分析一文里,我们知道了方法是如何缓存起来的,缓存的目的是为了加快方法的查找速度,那么我们在调用方法的时候是如何去读取这些缓存的呢?

下载本次分析用的demo,把main.m编译成main.cpp文件,找到main函数的实现。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 


        LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

    }
    return 0;
}
  • 可以看到,方法的调用在底层被解析成objc_msgSend方法。
  • objc_msgSend方法有两个参数,第一个参数为消息接收者,第二个参数是SEL

objc_msgSend流程分析

确定了objc_msgSend是我们的重点研究对象后,打开demo,全局查找一下,可以在objc-msg-arm64.s中找到_objc_msgSend的汇编实现,是的,_objc_msgSend是一个汇编方法。汇编指令可以参考这篇文章汇编指令查询

    ENTRY _objc_msgSend // ENTRY 标识_objc_msgSend入口
    UNWIND _objc_msgSend, NoFrame // UNWIND 无窗口

    cmp p0, #0          // cmp 判断指令,判断第一个参数,也就是接收者是否为空
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero // b.eq 等于0,则跳转到 LReturnZero
#endif
    // ldr 存储指令,把地址为x0的数据读入寄存器p13,x0是isa,p13 = isa
    ldr p13, [x0]  
    // 获取class存储到p16 p16 = class
    GetClassFromIsa_p16 p13     

LGetIsaDone:
    // 跳转到CacheLookup,开始查找缓存
    CacheLookup NORMAL, _objc_msgSend 

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    // 等于空,返回空
    b.eq    LReturnZero

    // 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:
    // mov传送指令,将#0的值传送给x1
    mov x1, #0 
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    // 退出 _objc_msgSend
    END_ENTRY _objc_msgSend

CacheLookup流程

.macro CacheLookup

LLookupStart$1:

    // 右移16位,获得_maskAndBuckets,存放到p11,p11 = mask|buckets
    ldr p11, [x16, #CACHE]

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
  // 通过掩码获取buckets,p11 & #0x0000ffffffffffff 后的值存放到p10, p10 = buckets
    and p10, p11, #0x0000ffffffffffff   
    // p11 右移48位,得到mask, p1 是_cmd(第一个参数sel),sel & mask得到哈希下标,并存放到p12
    and p12, p1, p11, LSR #48
    
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //32位真机才走这里,不用关心
    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

    // #define PTRSHIFT 3
    // p12是下标,p12, LSL #(1+PTRSHIFT)得到对应下标bucket的总偏移量大小
    // p10是buckets首地址,加上总偏移量,获取下标对应的bucket。
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     
    // ldp: 从x12取值,分别赋值给p17和p9,即p17=imp,p9=sel。
    ldp p17, p9, [x12]  
1:  cmp p9, p1          // 判断sel是否和传进来要判断的方法相等
    b.ne    2f          // 不等于 则跳转到2
    CacheHit $0         // 相等则返回 imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // 检查是否没找到,如果没找到则跳出循环
    cmp p12, p10        // 判断下标和首地址是否相等
    b.eq    3f          // 是首地址,则跳转到3
    ldp p17, p9, [x12, #-BUCKET_SIZE]! // 不是首地址,往前找一个bucket
    b   1b          // 跳转到1,进行下一次循环判断

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// p11是_maskAndBuckets,右移44位,得到buckets的最后一个元素的偏移量,p12现在也是buckets的首地址,进行偏移后移动到了最后一个元素。
    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.
       
    // 再查找一遍缓存
    // x12(即p12,当前为最后一个bucket)取值,分别把imp赋值给p17,sel给p9 
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // 判断sel是否和cmd相等
    b.ne    2f          //     不等跳转到2
    CacheHit $0         // 相等,返回imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // 检查bucket是否都找完了还是没找到
    cmp p12, p10        // 当前bucket是否为buckets首地址
    b.eq    3f          // 首地址跳转到3
    ldp p17, p9, [x12, #-BUCKET_SIZE]! // x12前移一个元素,取出前一个元素的imp和sel
    b   1b          // 进行下次循环判断

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0 // 没找到,则JumpMiss

.endmacro

objc_msgSend查找整体流程如下

objc_msgsend__