cache结构
objc_class 从objc_object继承一个8字节ISA指针,Class(typedef struct objc_class *) 类型superclass一个8字节指针,得出objc_class存在于内存中偏移16字节位置
从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 方法,通过方法猜测或查找进一步分析的可能信息
根据insert里的一些核心逻辑大概知道cache_t里核心结构可能是bucket_t结构,通过不断循环遍历set,同时根据前面打印cache_t内存结构,发现_occupied == 0
至此,找到 sel imp的存储逻辑
bucket_t 也查看到sel imp相关的内容
尝试sel imp查找获取
没有调用任何方法,缓存应该是空的
调用一次方法,缓存中应该有值,通过内存偏移,发现某个偏移位置,获取到了sel,虽然不是自定义的testFunc1,尝试再次执行一次testFunc1
此时在某个bucket_t指针某个偏移位置内存处拿到了 testFunc1, 在另一些位置还存在 description isNSString__ 这样无关的sel,分析可能bucket_t 指针存取时不是从第一个开始存的,位置比较随机,此处留存猜测暂且不表
根据提供的imp方法,第一个参数传入 bucket_t 指针,IFLObject.class作为第二个参数,从相同的偏移位置处获取到缓存的testFunc1的方法实现
以上lldb内存内容查看测试过程中,执行了两次方法调用,第一次没有获取到,第二次多次偏移有幸获取到了sel,可能是第一次没有,可能就是覆盖回收了,因为测试过程中顺便除了打印出了一些自定义方法缓存sel之外,还得到了 description isNSString__ 这些东西
缓存扩容
以上代码涉及到 realloc,也就是内存开辟,内存发生变化,与前面的猜测有些吻合,根据内存变化的代码逻辑
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
关于为什么 _occupied == capacity - 1, 从allocateBuckets源码知道,
arm架构,capacity最后多出来一个位置 sel存1,iml存 bucket 第一个位置的前一个位置的内存
x86架构,capacity最后多出来一个位置 sel存1,iml存 bucket 第一个位置
通过源码insert过滤,分析cache扩容机制
分别在insert 扩容判断前打印一次, 扩容判断后打印一次
IFLObject 实例对象第一次调用 testFunc1, 缓存里已经存在了两个sel,
导致newOccupied == 3, x86架构下达到了 扩容条件 3+1 > 3/4*capacity(4)
注意
扩容判断之前的打印,容器最后一个位置,也就是不可用的位置 sel == 0x1, 对应源码中的逻辑,iml存储的恰好就是 第一个bucket结构内存地址
扩容判断之后的打印,sel全部被清空
模拟底层源码cache结构分析 - 部分说明
模拟cache结构体
不调用任何方法 _occupied == 0
调用一个方法 bucket全遍历打印 _occupied == 2, 容量为4
调用两个方法 bucket全遍历打印 打印了8个内容, 出现testFunc2, testFunc1没有,说明 调用testFunc2时,触发两倍扩容,testFunc1被覆盖掉了