美文网首页
objc_msgSend快速查找流程

objc_msgSend快速查找流程

作者: xlii | 来源:发表于2022-08-11 17:15 被阅读0次

首先要知道,objc_msgSend 是汇编语言写的,区别于 C/C++ 实现的源码,优势是汇编语言非常快,对于方法查找这种经常发生的高频率事务,速度非常重要。

另外一点味了实现参数的不确定性(动态性),而 C\C++ 大多是静态方法,要实现动态性要更困难。

方法的查找过程

方法的查找可能非常困难,如果在类中找不到,则需要去超类中查找,如果还是找不到,则需要调用运行时的消息转发代码。

一般情况下,方法查找应该是非常快的,这与复杂的查找过程相冲突,而OC对这种冲突的解决方案是方法缓存

1.在前面分析 class的结构我们能知道,每个类都有一个缓存,它将方法存储为一对选择器和函数指针,它们被组织成一个哈希表,因此查找速度很快;

2.在查找方法时,首先会查询缓存,如果该方法不在缓存中,它会开始漫长而复杂的查找过程,最后将结果放到缓存中,以便下次查找更快;

总的来说,整个查找过程分为以下几部分:

1.根据isa找到Class

2.查找缓存

3.缓存未命中时,走慢查找

objc_msgSend 消息的接收者是对象,对象和方法的关系是 对象 —> isa —> 类(元类)—> cache_t —> methodList。

第1步:获取 isa 的类信息

objc4的源码中,搜索 objc_msgSend 在 objc-msg-arm.s 文件中找到 objc_msgSend 汇编实现,入口函数为:

Untitled.png

汇编实现如下:

  // 消息发送汇编入口:这一步主要获取 isa 类信息
    ENTRY _objc_msgSend
  // 无窗口化
    UNWIND _objc_msgSend, NoFrame

  // p0存放的是 objc_msgSend 的第一个参数-消息接收者receiver,receiver和空对比,判断消息接收者是否存在
    cmp p0, #0          // nil check and tagged pointer check

// 是否支持小对象类型,在arm64架构下,恒为true
#if SUPPORT_TAGGED_POINTERS

  // 1.支持则走小对象流程
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)

#else
  // 0.不支持则返回空
    b.eq    LReturnZero

#endif

  // p13存放isa
    ldr p13, [x0]       // p13 = isa

  // 从 p13 的 isa 中获取 class,放在 p16
    GetClassFromIsa_p16 p13     // p16 = class

// 获取isa完毕
LGetIsaDone:

    // calls imp or objc_msgSend_uncached
  // 开始从缓存中获取 imp地址(CacheLookup 方法参数为 NORMAL)
    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

GetClassFromIsa_p16 获取类信息的汇编实现:

.macro GetClassFromIsa_p16 /* src */

// 是否支持INDEXED_ISA
#if SUPPORT_INDEXED_ISA
    // Indexed isa
    // 将isa指针存入p16
    mov p16, $0         // optimistically set dst = src
    // 判断是否是 non-pointer isa
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    // 将_objc_indexed_classes 所在页的基址 读入x10寄存器
    adrp    x10, _objc_indexed_classes@PAGE
    // x10 = x10 + _objc_indexed_classes(偏移量),对x10基址根据偏移量进行内存偏移
    add x10, x10, _objc_indexed_classes@PAGEOFF
    // 将 p16 的 isa 从第 ISA_INDEX_SHIFT位(第二位)开始,提取 ISA_INDEX_BITS位(15位)到p16寄存器,剩余的高位用0补充
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

// arm64架构64位处理器
#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

主要流程是:

  1. 获取 isa 指针,判断是否为空,空则返回。
  2. 非空时判断是否支持 tagged pointer 小对象类型,支持则走小对象类型。不支持则返回空。
  3. 然后获取 isa 中的类信息,通过 isa & ISA_MASK 获取 bits 的 shiftcls 位域的类信息class.

第2步:CacheLookUp查找缓存

.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:

    // p1 = SEL, p16 = isa
  // 从 isa 平移16字节到 cache_t
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
  // p11 & 0x0000ffffffffffff 取出buckets放在p10
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
  // 将p11右移48位,得出mask,p1(_cmd) & mask 取出了缓存的下标 放在p12
    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

  // buckets首地址像左平移 _cmd下标 * 1<<4(16),得到所在bucket,放在p12
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

  // 从p12拿到imp、sel,存入p17、p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
  // 比较p9上的sel是否等于p1上的cmd
1:  cmp p9, p1          // if (bucket->sel != _cmd)
  // 如果不相等,则跳到2f
    b.ne    2f          //     scan more
  // 相等,则命中缓存,返回imp
    CacheHit $0         // call or return imp
    
// 2f,如果没有命中就走到这里
2:  // not hit: p12 = not-hit bucket
  // 判断 bucket 的 sel 是否为空,空则返回空
    CheckMiss $0            // miss if bucket->sel == 0
  // 比较 p12 的 bucket 和 p10 的 buckets 的首地址,也就是第一个 bucket,就是判断当前的bucket是否是第一个
    cmp p12, p10        // wrap if bucket == buckets
  // 相等,则跳转第三步
    b.eq    3f
  // 从p12的地址向前平移一个bucket的size,得到的bucket的imp和sel分别存在p17,p9
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
  // 重复第一步,对比sel和cmd
    b   1b          // loop

// 如果计算的下标是在第一个,则执行第3步
3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16

  // p11(mask|buckets) 右移 44 位,相当于 mask 左移 4 位,也就是 mask * 16,而mask是buckets最后一个元素的下标,16位存储着sel和imp的bucket的size,这一步相当于将地址平移到最后一个bucket,将该bucket存储于p12,缓存查找顺序是向前查找
    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.

  // 将最后的bucket的imp、sel分别放在p17和p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
// 比较p9的sel和传入参数cmd是否一致
1:  cmp p9, p1          // if (bucket->sel != _cmd)
  // 不一致则跳到步骤2f(继续往前查找)
    b.ne    2f          //     scan more
  // 一致则命中,返回imp
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
  // 如果从最后一个元素遍历过来,还是找不到就CheckMiss
    CheckMiss $0            // miss if bucket->sel == 0
  // 比较p12和第一个bucket p10 是否一致,也就是判断是否当前比较的是第一个 bucket了,再往前已经没有了
    cmp p12, p10        // wrap if bucket == buckets
  // 如果是第一个,则跳到3f
    b.eq    3f
  // 不是第一个,则向前平移 BUCKET_SIZE,找到前一个 bucket,imp、sel分别放入p17、p9
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
  // 返回第一步,继续对比sel和cmd
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
  // 跳转到 JumpMiss,因为是 normal,跳转至__objc_msgSend_uncached,表示缓存没找到
    JumpMiss $0

.endmacro

这一步是查找缓存,CacheLookup 传入的类型为 normal,arm64下主要流程为:

  1. 从 isa 平移16字节获取到 cache_t ,cache_t的首地址存放着 mask|buckets (32位处理器是buckets),放在一个8字节,将 mask|buckets & 0x0000ffffffffffff 得到了buckets,mask|buckets 右移48位,等到mask,通过 cmd & mask 取出缓存 bucket 的下标,cmd(sel) & mask 的算法是在方法缓存的时候计算哈希下标的算法,所以查找缓存也是用这个算法。
// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// Caches are never built in the dyld shared cache.

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}

buckets + ((_cmd & mask) << (1+PTRSHIFT)) 将 buckets 首地址向左平移下标*16。算出所在下标的 bucket。因为 sel 和 imp 各占8字节,即一个 bucket 的 size 为 16 字节。这样得到了 bucket 和 imp 和 sel。

步骤1:比较这个 bucket 的 sel 是否等于我们传入的cmd,如果不相等,则跳转到步骤2,相等,则缓存命中,返回 bucket 的 imp。

步骤2:判断当前 bucket 的 sel 是否为空,空就返回空,表示这个方法没有缓存。非空,则判断 bucket 是不是第一个 bucket,是第一个则跳转到步骤3,否则将 bucket 向前平移一个 BUCKET_SIZE,找前面的一个 bucket,返回执行步骤1。

步骤3:如果是第一个 bucket,将 mask|buckets 右移44位,相当于 mask 左移4位,也就是 mask*16,而 mask 是 buckets 最后一个元素下标,16为存储着 sel 和 imp 的 bucket 的 size,这一步相当于将地址平移到最后一个 bucket,获得最后一个 bucket 的 sel 和 imp,这步骤3里也分了3个分支。

3.注意这里是双层嵌套,下面是第二层

步骤3分支1:比较这个 bucket 的 sel 是不是等于传入的 cmd,不一致跳到分支2,一致则缓存命中,返回imp。

步骤3分支2:如果是从最后一个元素遍历过来的,当前的 bucket 的 sel 是 0,也就是这个槽没有缓存,则执行 CheckMiss,因为是 normal 类型,所以是__objc_msgSend_uncached,表示找不到缓存。如果 sel 不是0,判断这个 bucket 是不是第一个bucket,是的话就跳转到分支3,不是第一个,那么向前平移16个字节获取前面一个bucket,获取它的 sel 和 imp,返回执行分支1。

步骤3分支3:跳转 JumpMiss,因为是 normal 类型,跳转至 __objc_msgSend_uncached,表示缓存没找到。

如果经历 CacheLookUp 后没找到缓存,则会开始慢查找,从 methodList查找。

整个快速查找流程图:

Untitled 1.png

相关文章

网友评论

      本文标题:objc_msgSend快速查找流程

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