ios底层cache详解

13,301 阅读4分钟

cache结构

image.png

image.png

objc_class 从objc_object继承一个8字节ISA指针,Class(typedef struct objc_class *) 类型superclass一个8字节指针,得出objc_class存在于内存中偏移16字节位置

1.png 从objc_class 结构内存偏移16字节 获取到cache_t 内存结构,_bucketsAndMaybeMask(8字节) 和 一个联合体,union = {struct{_maybeMask(4字节) _flags(2字节) _occupied(2字节)} / _originalPreoptCache(8字节)}, 联合体内部 结构体 与 _originalPreoptCache(8字节)指针为互斥,所以cache_t 占据 8+4+2+2 = 16字节

从结构体成员分析,得不到有价值的分析信息,此时转入cache_t 方法,通过方法猜测或查找进一步分析的可能信息

image.png

image.png 根据insert里的一些核心逻辑大概知道cache_t里核心结构可能是bucket_t结构,通过不断循环遍历set,同时根据前面打印cache_t内存结构,发现_occupied == 0

image.png 至此,找到 sel imp的存储逻辑

image.png bucket_t 也查看到sel imp相关的内容

尝试sel imp查找获取

2.png

3.png 没有调用任何方法,缓存应该是空的

4.png 调用一次方法,缓存中应该有值,通过内存偏移,发现某个偏移位置,获取到了sel,虽然不是自定义的testFunc1,尝试再次执行一次testFunc1

5.png 此时在某个bucket_t指针某个偏移位置内存处拿到了 testFunc1, 在另一些位置还存在 description isNSString__ 这样无关的sel,分析可能bucket_t 指针存取时不是从第一个开始存的,位置比较随机,此处留存猜测暂且不表

6.png 根据提供的imp方法,第一个参数传入 bucket_t 指针,IFLObject.class作为第二个参数,从相同的偏移位置处获取到缓存的testFunc1的方法实现

以上lldb内存内容查看测试过程中,执行了两次方法调用,第一次没有获取到,第二次多次偏移有幸获取到了sel,可能是第一次没有,可能就是覆盖回收了,因为测试过程中顺便除了打印出了一些自定义方法缓存sel之外,还得到了 description isNSString__ 这些东西

缓存扩容

image.png 以上代码涉及到 realloc,也就是内存开辟,内存发生变化,与前面的猜测有些吻合,根据内存变化的代码逻辑

Group.png

1.开始 capacity = 4, 开辟4个单位大小的newBuckets
setBucketsAndMask(newBuckets, newCapacity - 1)
setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask){
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);
    _maybeMask.store(newMask, memory_order_relaxed);
    _occupied = 0;
}
_bucketsAndMaybeMask 存储分配的bucket结构的指针
_maybeMask 设置为3, _occupied 置0


    bucket_t *b = buckets();     // b 取得bucket结构首地址指针
    mask_t m = capacity - 1;     // m = 3
    mask_t begin = cache_hash(sel, m); // begin = xx & 3, 最大值不会超过3,也就是
得到索引的一个序列 0 - 3 中的一个序列值
    mask_t i = begin;      // i 从第一个取得的哈希索引位置开始遍历

    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();    // 如果遍历的位置没被占用,_occupied自增1
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) { // 不重复插入
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));
    // x86 向i后遍历 ,到达最后一个又跳到结构开始位置遍历,如果遍历位置 != begin,
    继续循环,否则bad_cache结束

2.第二次insert cache,newOccupied = 1 + 1, 
    x86架构 2 + 1 == 3/4 * 初始capacity(4),
    _occupied = 2, capacity = 4
    arm64 2 <= 3/4 * 4,继续遍历位置进行insert      
    _occupied = 2, capacity = 4
    
3.第三次insert cache,
      x86架构 newOccupied = 3,  3 + 1 > 3/4 * 4 触发两倍扩容
      _occupied = 1, capacity = 8
      arm64架构,newOccupied = 3, 3 == 3/4 * 4
      _occupied = 3, capacity = 4
4.第四次insert cache
    x86 newOccupied = 2, 2 + 1 < 3/4 * 8
    arm64, newOccupied = 4, 4 > 3/4 * 4, 
        并且 capacity4<8 && newOccupied4 <= capacyty4, 继续insert
5.第五次
    x86 newOccupied = 3, 3 + 1 < 3/4 * 8
    arm64, newOccupied = 5, 5 > 3/4 * 4,
        但 capacity4<8 && newOccupied5 > capacity4 触发两倍扩容
        newOccupied = 1, capacity = 8
6.第六次
    x86 newOccupied = 4, 4 + 1 < 3/4 * 8
    arm64, newOccupied = 2, 2 < 3/4 * 8
7.第七次
    x86 newOccupied = 5, 5 + 1 == 3/4 * 8, 
    arm64, newOccupied = 3, 3 < 3/4 * 8
8.第八次
    x86 newOccupied = 6, 6 + 1 > 3/4 * 8  触发两倍扩容
    _occupied = 1, capacity = 16
    arm64, newOccupied = 4, 4 < 3/4 * 8,  继续insert
9.第九次
    x86 newOccupied = 2, 2 + 1 < 3/4 * 16
    arm64, newOccupied = 5, 5 < 3/4 * 8, 继续insert
10.第十次
    x86 newOccupied = 3, 3 + 1 < 3/4 * 16
    arm64, newOccupied = 6, 6 == 3/4 * 8   继续insert
11.第十一次
    x86 newOccupied = 4, 4 + 1 < 3/4 * 16
    arm64, newOccupied = 7, 7 > 3/4 * 8,  8 <= 8 && 7 < 8, 继续insert
12.第十二次
    x86 newOccupied = 5, 5 + 1 < 3/4 * 16
    arm64, newOccupied = 8, 8 > 3/4 * 8,  
        但是 capacity8 <= 8 && newOccupied8 <= capacity8, 继续insert
12.第十三次
    x86 newOccupied = 6, 6 + 1 < 3/4 * 16
    arm64, newOccupied = 9, 9 > 3/4 * 8,  
        并且 capacity8 <= 8 && newOccupied9 > capacity8, 触发两倍扩容
        _occupied = 1, capacity = 16

根据cache 扩容机制,分析上面的sel打印结果,可以看出,第一次调用没有查找到并打印出sel,应该是因为 存在 description isNSString__ 这一类的insert,x86架构下insert == 2,就会触发 2 + 1 <= 3/4*4 两倍扩容,所以找不到,再次调用方法,方才第二次打印出了sel

image.png 关于为什么 _occupied == capacity - 1, 从allocateBuckets源码知道, arm架构,capacity最后多出来一个位置 sel存1,iml存 bucket 第一个位置的前一个位置的内存 x86架构,capacity最后多出来一个位置 sel存1,iml存 bucket 第一个位置

通过源码insert过滤,分析cache扩容机制

分别在insert 扩容判断前打印一次, 扩容判断后打印一次 7.png

9.png

10.png IFLObject 实例对象第一次调用 testFunc1, 缓存里已经存在了两个sel, 导致newOccupied == 3, x86架构下达到了 扩容条件 3+1 > 3/4*capacity(4)

注意 扩容判断之前的打印,容器最后一个位置,也就是不可用的位置 sel == 0x1, 对应源码中的逻辑,iml存储的恰好就是 第一个bucket结构内存地址

image.png 扩容判断之后的打印,sel全部被清空

模拟底层源码cache结构分析 - 部分说明

模拟cache结构体 11.png

不调用任何方法 _occupied == 0 12.png

调用一个方法 bucket全遍历打印 _occupied == 2, 容量为4 13.png

调用两个方法 bucket全遍历打印 打印了8个内容, 出现testFunc2, testFunc1没有,说明 调用testFunc2时,触发两倍扩容,testFunc1被覆盖掉了 14.png