OC 中调用方法的本质是消息的传递,通过 objc_msgSend
函数进行消息传递。那么在 objc_msgSend
的汇编流程中,最终会调用一个 CacheLookup
汇编函数,这是一个查找缓存方法的函数,那查找缓存方法的这个过程是怎么样的呢。
一、CacheLookup 的定义及参数介绍
这是在 objc_msgSend
中调用 CacheLookup
的代码:
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
CacheLookup
函数定义:
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
在汇编中,‘.macro
’ 代表宏定义。CacheLookup
是一个宏定义实现,它有四个参数,分别为 Mode,Function,MissLabelDynamic,MissLabelConstant。
- Mode:对应
objc_msgSend
调用中传的NORMAL
。 - Function:对应
objc_msgSend
调用中传的_objc_msgSend
。 - MissLabelDynamic:对应
objc_msgSend
调用中传的__objc_msgSend_uncached
。 - MissLabelConstant:这个参数没有传代表有默认值。
首先注意一点,在开始执行这个方法时,代码中有段注释:
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
p0
是传过来的消息接收者
,p1
是传过来的 sel
,p16
是传过来的 isa
,这点需要达成共识。
二、CacheLookup 流程
1、汇编代码分析
先来看第一段 CacheLookup
汇编实现的代码:
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
// - p1 = SEL, p16 = isa --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16。
// - p10 = mask|buckets -- 从 x16(即isa)中平移16字节,取出 cache 存入p10
// - isa 距离 cache 正好16字节:isa(8字节)- superClass(8字节)- cache(mask 高16位 + buckets 低48位)
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
- 这一段代码在通过内存平移的方式,取出
cache
,存入p10
。 p10
右移 48 位,取出mask
,并赋值给p11
。p10 & #0xffffffffffff
,得到buckets
并存入p10
。 但真机并不是走这里,这里贴出来只是为了方便下面的理解。
2、真机环境查找缓存方法的开始
真机走的是下面这一段:
// - 64位真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// - p11 = mask|buckets = cache
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
// - p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到 buckets 存入p10 -- 即去掉mask,留下buckets。
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// - p11(cache)右移48位,得到 mask(即 p11 存储 mask),mask & p1(msgSend的第二个参数 cmd)
// - 得到 sel 的下标 index(即搜索下标),存入p12。
// - cache insert 时的哈希下标计算是 通过 sel & mask,读取时也需要通过这种方式,objc-cache.mm 文件的 cache_hash 函数。
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
- 同样的,取出
cache
存入p11
。 p11 & #0x0000ffffffffffff
得到buckets
赋值给p10
。p11
右移 48 位,拿到mask
,mask & p1
得到下标
,存入p12
。- 为什么
mask & p1
就能得到下标,还记得那个insert
函数中用到的哈希算法取的下标吗,里面的实现和这一步是一样的,所以mask & p1
得到的就是当前p1
对应bucket
的下标。
这个时候,p10
是 buckets
的首地址,p11
是 mask
,p12
是下标
。
3、第一次 do while 循环查找缓存方法
拿到下标
和 buckets
之后,就开始,通过下标
取出 buckets
的 bucket
,看看缓存中是否有当前要找的 sel
。
汇编代码如下:
// 注意:LSL 指令代表左移,p12 是下标,(1+PTRSHIFT) 等于4。那么 p12, LSL #(1+PTRSHIFT) 相当于:下标左移4位(index << 4)
// add 指令代表相加,p10 是 buckets 的首地址,整句代码的意思是:p10 + 当前下标左移4位后的值存入 p13。
// sel 和 imp 占 8 字节,所以一个 bucket 占用 16 个字节。
// 那么 index << 4 中,index 代表从 buckets 的第一个下标到 index 的 bucket 的个数,左移 4 位代表一个 bucket 占 16 字节。
// 总体来说 index << 4 就是 index 个 16,这个时候 p13 等于 buckets 首地址平移 index 个 16 位后拿到的 bucket。
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)),PTRSHIFT等于3
// - 以下是 do while 循环,从 p13 开始往前遍历,如果 p13 前面的所以 bucket 找不到要查找的 sel,退出循环,继续往下走
// do {
// - 取出 bucket,并把 bucket 的 imp 赋值给 p17,sel 赋值给p9
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
// - 判断 p1(sel) 是否等于 p9(bucket 的 sel)
cmp p9, p1 // if (sel != _cmd) {
// - 如果不相等,跳转至 3f
b.ne 3f // scan more
// } else {
// - 如果相等 即 CacheHit 缓存命中,直接返回imp
2: CacheHit \Mode // hit: call or return imp
// }
// - 在 _objc_msgSend 调用 CacheLookup(当前方法)时,MissLabelDynamic 传的是 __objc_msgSend_uncached。
// - 所以如果 p9 = nil,跳转至 __objc_msgSend_uncached。
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
// 比较,是否取完,没有取完继续循环。
cmp p13, p10 // } while (bucket >= buckets)
// - 继续循环
b.hs 1b
- 这段代码需要重点注意!并且一定要理解
add p13, p10, p12, LSL #(1+PTRSHIFT)
这一行汇编代码的含义,具体请看代码的注释。 - 这行代码的目的是为了拿到要查找的
sel
的下标的bucket
,通过拿到的bucket
开始往前遍历查找是否有要查找的sel
。 - 通过
bucket >= buckets
判断是遍历到了buckets
的第一个元素,如果找到第一个元素还是没匹配到要查找的sel
,流程继续往下走。
4、根据环境,重新计算查找的下标。
前面的流程已经把要匹配的 sel
对应的下标开始往前查找 bucket
。这个时候已经查找完下标开始往前的 bucket
了,但没找到。
因为还没找完 buckets
中所有的 bucket
。那么接下来是不是该去查找下标往后的 bucket
了。
下面这一步就是重新计算查找的下标,请看这一段汇编代码:
// 这里是根据不同的环境,计算要开始重新查找的下标。
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
这一步是重新计算要开始重新遍历 buckets 的下标,以 CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS 环境下为例。还记得在前面探索的 insert 么,mask 等于 capacity - 1,那么 capacity 是什么,capacity 是 buckets 的大小!
那么这一小段代码相当于,把 buckets 的最后一个 bucket 存入 p13。
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
这一步是重新计算要开始重新遍历 buckets
的下标,以 CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
环境下为例。还记得在前面探索的 insert
么,mask
等于 capacity - 1
,那么 capacity
是什么,capacity
是 buckets
的大小!
这一小段代码相当于,把 buckets
的最后一个 bucket
存入 p13
。
5、第二次 do while 循环查找缓存方法
重新计算好下标后,会进行第二次 do while
循环的查找,汇编代码如下:
// 注意看这里,在来到这之前,p12 是下标,是上面第一次循环开始的下标,那么它通过下标找到下标对应的 bucket,并且将 bucket 存到 p12。这个时候,p12 变成上面第一次开始循环的 bucket。
// 当前的 p13 存着的 bucket 是往前遍历 buckets 的开始,通过 (sel != 0 && bucket > first_probed) 判断,是否遍历到了第一次循环遍历的临界点。
// 如果到达临界点,走下一个流程 __objc_msgSend_uncached。
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// - 以下是 do while 遍历,遍历 buckets,获取每个 bucket,查找是否有需要的 sel-imp
// do {
// - 取出 bucket,并把 bucket 的 imp 赋值给 p17,sel 赋值给p9
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
// - 判断 p1(sel) 是否等于 p9(bucket 的 sel)
cmp p9, p1 // if (sel == _cmd)
// - 如果相等 即 CacheHit 缓存命中,跳转到 CacheHit 方法
b.eq 2b // goto hit
// - 如果不相等,并且 sel 不等于 nil,bucket > first_probed,继续循环
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
LLookupEnd\Function:
LLookupRecover\Function:
// 跳转至 MissLabelDynamic(__objc_msgSend_uncached)
b \MissLabelDynamic
这一步的流程和第一次的 do while
方式是一样的,只是判断的条件有变化。
因为第一次 do while
的时候,部分方法已经查找过了,为了避免重复查找,通过 (sel != 0 && bucket > first_probed)
条件判断,是否遍历到了第一次循环遍历的临界点。
如果到达临界点,并且还是没有匹配到 sel,就走下一个流程 __objc_msgSend_uncached
。
三、CacheHit - 缓存命中
如果在快速查找缓存方法的这个过程中,匹配到了 sel
,就会执行 CacheHit
(缓存命中),就是汇编代码的 2:
这一步。
// 调用或返回imp
2: CacheHit \Mode // hit: call or return imp
CacheHit
的汇编实现如下:
//- 缓存命中
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
$0
是传进来的 Mode,那 Mode 的值是什么,Mode 的值是 CacheLookup
传进来的 Mode,Mode 的值为 NORMAL
。
所以在 CacheLookup
中调用 CacheHit
走的是第一个判断,验证并且调用 IMP
。
以上就是快速查找缓存方法的流程,如果在快速查找缓存方法的流程里匹配不到 sel
,就会进入下一步:__objc_msgSend_uncached
。