这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
补充:在lldb中调用方法为什么mask为7
我们看到在代码中调用方法的情况,那么在lldb中调用方法呢?试一下:
这里的mask变成了7,这是为什么呢? mask变成了7,那么代表着,cache一定进行了一次类似的扩容,那么其必然就会到insert里面,那么我们去insert实现里面。
之前证明了,当缓存方法到了3/4 的时候,就会进行扩容,那么是不是我们在lldb中调用方法的时候,会同时调用其他方法呢,我们来看一下,在insert中打印一下sel,imp 和 receiver,这样就会打印所有的方法。运行一下。
然后在lldb中调用对象方法,看看输出。
我们想要找的情况是receiver是p的情况,所以我们打印一下p的地址。
然后在上面的打印结果中寻找,发现这三个方法的receiver都是p。
所以我们发现,在我们调用saySomething方法之前,会先调用 respondsToSelector以及class 方法,所以在插入saySomething的时候会进行扩容,所以mask变为7.
作业:代码运行方法会不会插入结束符号
代码中调用方法,这里调用两次 在insert里面把bucket的sel,imp和地址打印出来。
这里saySomething之前调用setName是因为上面的打印方法放在开辟空间之前,如果不先调用空间还没开辟打印不出来东西,因为oldCapacity会是0,运行一下。
打印结果可以看到,还是有结束符号的。
这里的0x7fff7c838d69是setName 方法,因为saySomething在下面才插进去。
接着看insert 里面的插入。
这里有一个do while 循环,上次说到这里,当插入的时候如果找到一个值,发现里面有人,就会进行cache_next。看一下cache_next内部是什么样的。
发现这里如果发现有值在里面的话,如果i不等0,就往前移一位重新插入,否则就会到mask开始重新插入,那么什么是mask呢。前面看到传进来的是m,往前找,发现m是:
大致流程图:
那么会不会有一种情况,就是一直找不到空位呢?如果一直找不到,那么就会退出循环,然后调用bad_cache。
bad_cache(receiver, (SEL)sel);
再看一下set方法。
发现是先存imp,在存sel。
objc_msgSend流程
上文讲解了objc_msgSend 汇编源码,得到了以下流程:
- 判断receiver是否存在
- 通过receiver获取isa,进而获取class
现在已经获取到了class了,那么class要拿来干什么呢? 这里看到拿到了class之后,调用了 CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached 这个函数
接着我们去搜索一下CacheLookup 的实现。这里把之前的函数复制过来,一一对应发现少了一个参数,那么说明最后一个参数是默认参数。
往下看代码:
// 之前的p16是class,这里是把class放到x15里
mov x15, x16
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
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
// 因为是真机,arm64,所以是CACHE_MASK_STORAGE_HIGH_16,看解释1.
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 把x16 平移#cache大小,也就是16个字节,看解释2.所以就得到cacht_T.
//然后把的到的结果放到p11,所以p11 = cache
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// 接下来进入到这里,原因看解释3
#else
// p11 与上 #0x0000fffffffffffe得到bucket,放到p10里面。
//原因看解释4
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//判断p11和0,不为0,也就是存在则跳转LLookupPreopt,否则往下走
tbnz p11, #0, LLookupPreopt\Function
#endif
// p11 不为0 走到这里
// p12位空,p1为sel,这里的操作是这样(_cmd ^ (_cmd >> 7))
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
// p11 是cache,与上0x0000ffffffffffff得到bucket
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//p11, LSR #48 ->mask
// p1:sel(_cmd) & mask 得到index存入p12. 原因看解释5
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
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
//
// p10是bucket,p12是index,PTRSHIFT是3,看解释6,所以就是index左移4位。因为bucket + 内存平移,只能平移 1,2,3,4单位,不能平移0x10XXXXX地址,所以((_cmd & mask)左移4位相当于index * 16(bucket一个单位的大小),这样就会得到在bucket中对应的地址。因此,p13就是当前要查找的bucket
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
//从x13(即p13 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
// 拿查到的sel p9 和 要查的 p1 对比
//一样就到CacheHit(缓存命中)
//不一样就到3里面
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
// 判断p9是否为空。
//为空就去MissLabelDynamic
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
// 对比p10 和 p13 大小,如果p13大于p10,就地址平移,也就是再次哈希的-1,然后从1:开始循环,不大于则继续往下走
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//如果上面查找不到这里就会把bucket平移 mask<<4 的结果的位数,然后放到p13,对应了在哈希的过程看解释7
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
CacheHit(缓存命中)
.macro CacheHit
// LLookupStart参数为normal 进入这里
.if $0 == NORMAL
// x17为cached imp,x10是bucket的地址,x1是要查找的sel,x16是isa
// 进入到TailCallCachedImp,进行x17异或x16,看解释8然后执行imp
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
MissLabelDynamic
如果找不到就会进入到MissLabelDynamic,也就是之前传过来的第三个参数__objc_msgSend_uncached
__objc_msgSend_uncached:流程
STATIC_ENTRY __objc_msgSend_uncached
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band r9 is the class to search
MethodTableLookup NORMAL // returns IMP in r12
bx r12
END_ENTRY __objc_msgSend_uncached
解释1
从这里可以看到,当是__arm64__(支持64位)且是__LP64__(unix或者unix类的系统-linux,Mac OS X的情况下,如果是#if TARGET_OS_OSX || TARGET_OS_SIMULATOR)(macOS或者macOS模拟器)的时候是CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS,不然就是CACHE_MASK_STORAGE_HIGH_16,所以这里是CACHE_MASK_STORAGE_HIGH_16。
解释2
在文件中搜索cache,发现他是2 * SIZEOF_POINTER 也就是16.
解释3
在源码中寻找CONFIG_USE_PREOPT_CACHES,发现其定义
因为这里是64位,而且是os系统,不是模拟器也不是MACCATALYST,所以CONFIG_USE_PREOPT_CACHES是1,进循环。 has_feature(ptrauth_calls): 是判断编译器是否支持指针身份验证功能,a12Z以上的芯片。所以这里是false。所以进下面的else。
解释4
mask和buckets放在一起共占用8个字节,64位。 0x0000fffffffffffe转化为2进制为 111111111111111111111111111111111111111111111110,是48位。 再来看cache_t的结构,maskShift也是48,第1到48位存放了maskshift,而bucketsMask是就存在了高端的16位。这里p11 cache & 0x0000fffffffffffe 将高16位mask抹零,得到了bucket。
解释5
我们看在insert是如何取第一个下标的,在源码中看到,insert里面是通过sel&mask得到的index。
解释6
解释7
这里看到如果i等于0的时候,那么就会直接取mask的值。
解释8
进入TailCallCachedImp流程如下
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
// x17异或isa(类)
// call imp
eor $0, $0, $3
br $0
.endmacro
流程总结
- 判断receiver是否存在
- 通过receiver获取isa,进而获取class
- 进入 CacheLookup,通过class内存平移获取到cache
- 通过bucket掩码得到bucket
- 通过mask掩码得到mask
- 通过insert 哈希函数 (mask_t)(value&mask)获取到第一次查找的index
- bucket + index 找到 应该要找的那个bucket
- bucket里面有imp和sel,拿到查找到的sel和之前传进来的sel对比,相等就cacheHit,imp^isa解码,然后调用imp,否则就再次平移,找到就cacheHit,找不到就继续循环。如果一直没有找到就到__objc_msgSend_uncached。