iOS进阶 -- cache_t原理下

642 阅读5分钟

前言

在上一篇 cache_t原理 的探索中,还有几个问题没有探索,本篇继续就这几个问题进行探索,主要包含一下几点:

  • cache_t原理
    • _bucketsAndMaybeMask 探索
    • 计算capacity时,为何要 mask 加 1
    • 存取imp时的编解码

1、_bucketsAndMaybeMask

cache_t原理上 中主要使用到的是 _maybeMask 和 _occupied,这一篇就从 _bucketsAndMaybeMask 的作用开始进行探索。

首先还是使用 BPPerson 为例进行 LLDB 调试。其代码和结果如下:

Xnip2021-07-04_13-59-42.png

从图中只得到 _bucketsAndMaybeMask 的一个地址,并没有其他信息。于是全局搜索用到 _bucketsAndMaybeMask 的地方,发现有很多地方调用,但查看一遍后,可以发现在 setBucketsAndMaskbuckets 函数中的用法如下:

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 相同,其结果如下:

Xnip2021-07-04_14-11-11.png

结果表明 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取值。

在其他架构下可以看到还是用到了 maskShiftmaskMask 等,其实这些都是 cache_t 结构体中定义的 static 成员。如下图所示:

Xnip2021-07-04_15-15-26.png

CACHE_MASK_STORAGE_HIGH_16 为例,maskShift=48表明如果要取mask,只要将 _bucketsAndMaybeMask 右移 48位即可,也对应了该架构下 mask() 函数的实现。

对于本次测试的 CACHE_MASK_STORAGE_OUTLINED,则只定义了 bucketMask,使用时直接使用 _maybeMask 即取到 mask的值,而取 bucket时,将 _bucketsAndMaybeMaskbucketMask 进行与运算即可。

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 调试可以验证如下:

Xnip2021-07-04_17-06-43.png

Xnip2021-07-04_17-10-45.png

图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 调试过程中还有一个现象比较奇怪,代码和调试结果如下:

Xnip2021-07-04_17-22-33.png

person在代码中还未调用方法时打印 cache_t 得到的结果发现 _maybeMask 结果为 0,因为此时没有调用过方法,缓存空间还没开辟。但是使用 LLDB 指令调用 task2 方法后,再次查看却发现 _maybeMask 直接变为了7,而不是上一篇中说到的3。这一系列迷之操作,究竟是什么原因呢?

为什么使用 LLDB 调试时第一次调用方法_maybeMask变为了7?首先验证下使用LLDB调试时,会不会走我们编译好的可调试源码。在缓存方法的 insert 函数中写下如下代码:

Xnip2021-07-04_21-42-12.png

由执行结果可以看出,LLDB调试也会走此源码。于是可以在代码中加入一下代码:

Xnip2021-07-04_22-07-47.png

执行结果如下图:

Xnip2021-07-04_22-08-18.png

在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打印出方法名,结果如下:

Xnip2021-07-04_22-12-07.png

可以发现这里是调用了 respondsToSelector:class 方法,因此 occupied 为 2,newOccupied为3。

这里 _maybeMask 明明为 7,在计算 capacity 时为何需要加 1 呢?在上面的打印结果中,我们可以发现在最后打印的是 0x1,其 bucket 指针指向与第一个的指向一致。在 allocateBuckets 函数中也可以发现如下代码定义:

Xnip2021-07-04_22-47-35.png

这段注释说明,在开辟 bucket 的存储空间时,会先默认在末尾插入一个 1,其imp指向第一个bucket。由此说明了为何在计算 capacity 时,要进行加1操作了。其实这里加1,也是为了给 bucket 设定一个边界,用于区分是否到达了末尾。

至此,关于cache_t 的探索补充就完成了,对于文中出现的不足之处欢迎大家指正,也欢迎大家继续关注后续的探索。