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

方法是如何读取的?

[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查找整体流程如下