前言
在上一篇 cache_t原理 的探索中,还有几个问题没有探索,本篇继续就这几个问题进行探索,主要包含一下几点:
- cache_t原理
- _bucketsAndMaybeMask 探索
- 计算capacity时,为何要 mask 加 1
- 存取imp时的编解码
1、_bucketsAndMaybeMask
在 cache_t原理上 中主要使用到的是 _maybeMask 和 _occupied,这一篇就从 _bucketsAndMaybeMask 的作用开始进行探索。
首先还是使用 BPPerson 为例进行 LLDB 调试。其代码和结果如下:
从图中只得到 _bucketsAndMaybeMask 的一个地址,并没有其他信息。于是全局搜索用到 _bucketsAndMaybeMask 的地方,发现有很多地方调用,但查看一遍后,可以发现在 setBucketsAndMask 和 buckets 函数中的用法如下:
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
uintptr_t buckets = (uintptr_t)newBuckets;
uintptr_t mask = (uintptr_t)newMask;
ASSERT(buckets <= bucketsMask);
ASSERT(mask <= maxMask);
_bucketsAndMaybeMask.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, memory_order_relaxed); // 存储 newBuckets
_occupied = 0;
}
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask); // 利用 _bucketsAndMaybeMask 取出的值得到 buckets
}
在这两段代码中发现 _bucketsAndMaybeMask 的使用均与 bucket 有关。于是继续 LLDB 查看 buckets(),看下获取到的首地址是否与 _bucketsAndMaybeMask 相同,其结果如下:
结果表明 buckets() 的首地址与之前打印的 _bucketsAndMaybeMask 地址一抹抹一样样,均为 0x00000001003623d0,于是可以证明我们的结论,_bucketsAndMaybeMask 表示 buckets() 首地址。
但是 _bucketsAndMaybeMask 的意义仅仅在于此吗? 显然不是。其实 _bucketsAndMaybeMask 是 bucket 和 maybeMask 的合体,在使用到时,会与一个mask进行与运算得到相应的值。
在上一篇文章探索 mask() 时,发现该函数有多个地方定义,其实这是对应的不同架构下的函数,例如:
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
mask_t cache_t::mask() const
{
return _maybeMask.load(memory_order_relaxed);
}
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 || CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
mask_t cache_t::mask() const
{
uintptr_t maskAndBuckets = _bucketsAndMaybeMask.load(memory_order_relaxed);
return maskAndBuckets >> maskShift;
}
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
mask_t cache_t::mask() const
{
uintptr_t maskAndBuckets = _bucketsAndMaybeMask.load(memory_order_relaxed);
uintptr_t maskShift = (maskAndBuckets & maskMask);
return 0xffff >> maskShift;
}
#else
#error Unknown cache mask storage type.
#endif
从这些不同架构下的 mask() 函数,可以发现对于mask 的取值时不一样的,有些只用到 _maybeMask,有些则需要用到 _bucketsAndMaybeMask。本次测试使用的 Mac OS,这里会走 CACHE_MASK_STORAGE_OUTLINED 下的分支,即直接用 _maybeMask取值。
在其他架构下可以看到还是用到了 maskShift 和 maskMask 等,其实这些都是 cache_t 结构体中定义的 static 成员。如下图所示:
以 CACHE_MASK_STORAGE_HIGH_16 为例,maskShift=48表明如果要取mask,只要将 _bucketsAndMaybeMask 右移 48位即可,也对应了该架构下 mask() 函数的实现。
对于本次测试的 CACHE_MASK_STORAGE_OUTLINED,则只定义了 bucketMask,使用时直接使用 _maybeMask 即取到 mask的值,而取 bucket时,将 _bucketsAndMaybeMask 与 bucketMask 进行与运算即可。
2、存取imp时的编解码
在缓存方法时,其实缓存的就是 sel 和 imp,两者均存在 bucket_t 结构体中。上一篇提到在存取 IMP 时,还做了其他操作,本节我们就来探索下。
首先看下 bucket_t 存取 imp 的源码,分别对应 set 和 imp 函数,代码如下所示:
// 取 imp
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls); // 本次测试所用架构
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
存储imp 时会调用 set 函数,该函数会先调用一个 encodeImp 函数,代码如下所示:
// Sign newImp, with &_imp, newSel, and cls as modifiers.
uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
return (uintptr_t)
ptrauth_auth_and_resign(newImp,
ptrauth_key_function_pointer, 0,
ptrauth_key_process_dependent_code,
modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (uintptr_t)newImp ^ (uintptr_t)cls; // 本次测试所用架构
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
}
对比代码可以发现,在存储和读取时,均作了异或运算。
存储时存的是: imp ^ cls
读取时读的是: 读取的imp ^ cls
其实这是利用异或运算的一个特性 a = a ^ b ^ a,这里 cls 就充当了 b 的角色,这样做的目的是给存储的值进行加盐。以上代码通过 LLDB 调试可以验证如下:
图1所示为第一次调用方法进行缓存时存储的结果,图2为第二次调用后通过 LLDB 读取缓存的值。第一次存储的值为 47472,第二次读取到该值后,使用该值异或class,得到的结果正好时第一次存储时newImp的值,由此可以验证Imp存储时的编解码操作。
3、capacity的计算为何要 mask 加1
在缓存方法前,先要获取cache_t当前的容量,但是在查看 capacity() 函数时,发现每次都要 mask + 1。代码如下:
unsigned cache_t::capacity() const
{
return mask() ? mask()+1 : 0;
}
而且在 LLDB 调试过程中还有一个现象比较奇怪,代码和调试结果如下:
person在代码中还未调用方法时打印 cache_t 得到的结果发现 _maybeMask 结果为 0,因为此时没有调用过方法,缓存空间还没开辟。但是使用 LLDB 指令调用 task2 方法后,再次查看却发现 _maybeMask 直接变为了7,而不是上一篇中说到的3。这一系列迷之操作,究竟是什么原因呢?
为什么使用 LLDB 调试时第一次调用方法_maybeMask变为了7?首先验证下使用LLDB调试时,会不会走我们编译好的可调试源码。在缓存方法的 insert 函数中写下如下代码:
由执行结果可以看出,LLDB调试也会走此源码。于是可以在代码中加入一下代码:
执行结果如下图:
在LLDB执行 [person task2] 后,此时的值变为:
- newOccupied 变为了 3,capacity 为4
- 是否需要扩容的条件是 newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity),
- CACHE_END_MARKER 定义为 1
因此判断条件为 4 <= 3,很显然需要扩容,而扩容后容量变为了 8,_maybeMask 自然就变为了 7,缓存的方法数变成了 1。
这里为什么会扩容呢?看下打印的方法可以看到在调用 task2 时,已经有了两个方法。根据其sel打印出方法名,结果如下:
可以发现这里是调用了 respondsToSelector: 和 class 方法,因此 occupied 为 2,newOccupied为3。
这里 _maybeMask 明明为 7,在计算 capacity 时为何需要加 1 呢?在上面的打印结果中,我们可以发现在最后打印的是 0x1,其 bucket 指针指向与第一个的指向一致。在 allocateBuckets 函数中也可以发现如下代码定义:
这段注释说明,在开辟 bucket 的存储空间时,会先默认在末尾插入一个 1,其imp指向第一个bucket。由此说明了为何在计算 capacity 时,要进行加1操作了。其实这里加1,也是为了给 bucket 设定一个边界,用于区分是否到达了末尾。
至此,关于cache_t 的探索补充就完成了,对于文中出现的不足之处欢迎大家指正,也欢迎大家继续关注后续的探索。