objc_msgSend快速查找流程

首先要知道,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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容