引言
通过上篇文章我们将源码编译成.cpp文件,发现方法的本质其实就是底层调用了objc_msgSend(receive, sel)进行消息发送,那么objc_msgSend()在底层做了什么?接下来一起分析一下
汇编分析
objc_msgSend() 是在libobjc.A.dylib库中,我们全局搜索objc_msgSend,(然后按住command将搜索结果折叠起来,因为结果太多,不利于我们分析究竟该看哪个文件),这里我们使用的是模拟器,所以选用objc-msg-arm64汇编文件,找到ENTRY _objc_msgSend入口,如下图:
下面一步步分析汇编代码:
p0就是objc_msgSend(receiver, sel)中的第一个参数receiver,这里比较了一下方法的调用者是否为nil
cmp p0, #0 // nil check and tagged pointer check p0:receive
x0是对象的内存地址,将x0放到了p13中,调用GetClassFromIsa_p16方法,传递了p13, 1, x0三个参数
ldr p13, [x0]
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
2.1 GetClassFromIsa_p16方法接受了三个参数,分别是对象的内存地址src = p13,是否需要授权 needs_auth = 1,授权地址 auth_address = x0,其中__LP64__是我们常见的情况,又因为需要授权,所以调用了ExtractISA p16, \src, \auth_address方法,传递了对象的内存地址
// src=p13, needs_auth=1, auth_address=x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA
... // 暂时不分析这种case
#elif __LP64__ // 这个是我们常见的case
.if \needs_auth == 0
mov p16, \src
.else // needs_auth 传递过来的值为1, 所以走的是ExtractISA方法
// 64-bit packed isa
ExtractISA p16, \src, \auth_address
.endif
#else
...
#endif
.endmacro
2.2 这里的$0 = p16, $1 = src(对象的内存地址), $2 = #ISA_MASK,拿着$1 & #ISA_MASK,也就是对象的内存地址 & isa掩码 = 类的内存地址
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
小结:GetClassFromIsa_p16方法就是通过对象的内存地址获取到类地址的过程。
CacheLookup方法要么调用imp,要么就是没有找到方法,则调用__objc_msgSend_uncached
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
CacheLookup方法实现
// CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
... // 不常用case,这里不做研究
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE] // p11 = mask|buckets cache_t*
#if CONFIG_USE_PREOPT_CACHES
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
/**
p1: sel
p11: cache
LSR: 右移
eor: 异或
mask: 高16位
bucketmask: 低48位
p12 = (p1 ^(p1 >> 7)) 等价与 value = sel ^(sel >> 7)
p12 = p12 & (p11 >> 48) 等价与 index = value & (mask)
*/
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
... // 不常用case,这里不做研究
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
... // 不常用case,这里不做研究
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
LLookupEnd\Function:
逐行分析:
4.1 x16 是类的内存地址,将x16移到了x15中
mov x15, x16 // stash the original isa
4.2 获取buckets。
x16是内存地址,将x16偏移#CACHE个大小,赋值给p11,全局搜索发现CACHE,发现#define CACHE (2 * __SIZEOF_POINTER__) ,2个指针的大小即16,我们之前有讨论过类地址偏移16位是cache_t,那么p11就是cache_t的内存地址
p11 & #0x0000fffffffffffe = p10 即cache地址& bucket掩码 = buckets
ldr p11, [x16, #CACHE] // p11 = mask|buckets cache_t
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
4.3 获取查找buckets的index。
eor 异或,不同为真,同为假。LSR 向右移位。
eor p12, p1, p1, LSR #7 将p1 << 7 位异或p1最后赋值为p12,即p12 = p1 ^ (p1 << 7),还记得这里p1是谁么,就是objc_msgSend(receover, sel)中的sel即p12 = sel ^ (sel << 7)
and p12, p12, p11, LSR #48 ,p11是cache地址,cache << 48 可以获取到mask值,结合上面得出的p12,可总结为:mask & sel ^ (sel << 7) = p12
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48
小结:以上两句和inset()函数中cache_hash()计算下标用的是同一种逻辑,如下图:
buckets偏移index个大小。
上面已经计算出index,可是它只是一个数值,例如1,无法和p10(buckets)内存地址相加,所以就将数值转换成了内存地址,向左移了4,例如0001 << 4 => 1000,因为bucket中包含了sel和imp正好16位。则bucket = buckets & (index << 4) = p13
add p13, p10, p12, LSL #(1+PTRSHIFT)
- 根据
sel寻找对应的imp,找不到则走MissLabelDynamic
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // sel != _cmd
b.ne 3f // scan more
2: CacheHit \Mode // hit: call or return imp
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10
b.hs 1b
6.1
第一部分,将x13(bucket)中的sel和imp分别赋值给p9和p17,然后比较sel和传入的_cmd是否相等,相等则跳转到第二部分,不相等则跳转到第三部分。再次来到这里时进行*bucket--移动。
第二部分,CacheHit 命中,返回imp
第三部分,判断sel == 0 ,为0 则走MissLabelDynamic,
不为0,则比较当前的bucket是否等于buckets ,如果不等则跳转到第一部分,进行指针前移。