首先要知道,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 汇编实现,入口函数为:
汇编实现如下:
// 消息发送汇编入口:这一步主要获取 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
主要流程是:
- 获取 isa 指针,判断是否为空,空则返回。
- 非空时判断是否支持 tagged pointer 小对象类型,支持则走小对象类型。不支持则返回空。
- 然后获取 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下主要流程为:
- 从 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查找。
整个快速查找流程图: