OC 底层探索 07、类的结构分析2 - cache_t
本文来探索类结构中 cache_t.
之前的文章OC底层探索04 中,已知如何找到类信息。本文我们对类信息中的 cache_t 进行探索。
objc_class 结构 :
从 OC底层探索04 中的指针和内存偏移,我们已知可通过指针平移获取相应位置信息,cache_t 的位置 = 8 + 8 =16
一、cache_t 简析
cache_t 的源码分析:
CACHE_MASK_STORAGE:
1、支持架构
cache_t 源码有点长,我们可从截取的这部分代码中看到它对不同架构的支持:
MacOS:i386
模拟器:x86
真机:arm64
cache_t 中还可以发现一点,模拟器和真机的一些处理是不同的,业务开发中,我们所调试使用的最好的选择还是真机。
2、cache_t 内容
cache_t --> 缓存 --> 增删改查
模拟器:
bucket_t:
explicit_atomic --> 我们点击进去可以看到 它是一些 C++ 代码,而其中重要的内容是 ‘T’ --> struct bucket_t * 。关于它,在这里我们暂时只需要知道它是原子性,为了我们缓存的安全性即可,更深层的后续再做探究。
struct bucket_t *:imp sel
_mask
真机 64:
从下面代码,可观察到 maskAndBuckets 和一系列带有‘mask’ 的字段 --> 掩码、指针平移
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // 真机64 explicit_atomic<uintptr_t> _maskAndBuckets; mask_t _mask_unused; // How much the mask is shifted by. static constexpr uintptr_t maskShift = 48; // Additional bits after the mask which must be zero. msgSend // takes advantage of these additional bits to construct the value // `mask << 4` from `_maskAndBuckets` in a single instruction. static constexpr uintptr_t maskZeroBits = 4; // The largest mask value we can store. static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1; // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer. static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1; // Ensure we have enough bits for the buckets pointer. static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
_maskAndBuckets:--> bucket_t .
_mask_unused:可能是苹果的预留,不管它 <-- "Don't know how to do ... ..." .
另:
_flags:标记
_occupied:占位,内存占多少
--> cache_t 结构图:
二、cache_t 缓存了什么
1、cache_t 缓存了方法
运行工程(部分测试代码可能存在偏差,可自行编写),在未调用任何方法前,cache_t 内容:
标线所示的值均为 0,继续执行,p 调用方法:
对象 p 调用一次方法后,sel imp 不再为0,_mask 3 、occupied+1 -->
推测:方法执行一次后缓存在 cache_t 中。(mask occupied 文章后半部分探究)
验证 _buckets 中存着调用过的方法:
cache_t 源码中寻找是否有获取 _buckets 的方法 :
继续 lldb 调试:
上图,可验证 --> 方法首次执行后缓存在 cache_t 中:
cache_t 中 sel 就是 对象p刚刚所调用方法的方法名,
imp 指向是MyPerson中的 方法的指针,指针地址0x0000000100001b50.
2、cache_t 缓存集合 - buckets
多个方法调用
我们继续运行代码,让p调用方法2:
由上可知:方法调用后都会存 buckets 中。
同样通过OC底层探索04的指针和内存偏移,通过数组 index 属性操作:
同样,我们取到了方法。
思考:方法再调用会怎么样呢?
方法只会缓存一份,方法调用的流程是什么样子的呢? --> 后续文章再对 objc_msgSend 流程进行探究。
2、cache_t 中 mask 和 occupied 是什么?
运行工程,调用多个方法,进行 lldb 调试. 如下图:
调试过程中,我们发现了几个问题:
1、occupied 和 mask 是什么?它们的值为何是一直在变化的?
2、cache_t 中方法的顺序和调用方法顺序为何不同?
3、buckets 中方法为何丢失不在了?
寻找答案:
1、去 cache_t 源码:
进入 mask() 和 occupied() 方法,发现没什么有用信息!
但看到下面 incrementOccupied() - occupied 增量:
源码中我们发现了 _occupied++ 和 mask() 的操作.
全局搜索 ‘incrementOccupied( ’ --> cache_t 的insert 中做了 occupied/mask 的处理
2、cache_t::insert 方法流程:
1 ALWAYS_INLINE 2 void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver) 3 { 4 #if CONFIG_USE_CACHE_LOCK 5 cacheUpdateLock.assertLocked(); 6 #else 7 runtimeLock.assertLocked(); 8 #endif 9 10 ASSERT(sel != 0 && cls->isInitialized()); 11 12 // Use the cache as-is if it is less than 3/4 full 13 mask_t newOccupied = occupied() + 1;// occupied():return _occupied; --> _occupied + 1 14 unsigned oldCapacity = capacity(), capacity = oldCapacity; // _mask.load() --> capacity 空间 15 if (slowpath(isConstantEmptyCache())) { 16 // 初始化,occupied=0,buckets()是空 17 // Cache is read-only. Replace it. 18 if (!capacity) capacity = INIT_CACHE_SIZE;// capacity 给1<<2的空间 4 19 // 真正去向系统开辟内存 20 reallocate(oldCapacity, capacity, /* freeOld */false); 21 } 22 else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { 23 // Cache is less than 3/4 full. Use it as-is. 24 // cache < 3/4 capacity 25 } 26 else { 27 capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 扩容 如果capacity 不为空 扩容为当前的2倍;为空则去开辟 4 28 if (capacity > MAX_CACHE_SIZE) {// 空间最大 1<<16 = 2^16 29 capacity = MAX_CACHE_SIZE; 30 } 31 reallocate(oldCapacity, capacity, true);// 重新开辟空间,true:旧的free 32 } 33 34 bucket_t *b = buckets(); 35 mask_t m = capacity - 1;// 2^2-1=3 2^3-1=7 36 mask_t begin = cache_hash(sel, m);// sel & mask 37 mask_t i = begin; 38 39 // Scan for the first unused slot and insert there. 40 // There is guaranteed to be an empty slot because the 41 // minimum size is 4 and we resized at 3/4 full. 42 do { 43 // 位置是空的可以放 44 if (fastpath(b[i].sel() == 0)) { 45 incrementOccupied(); 46 b[i].set<Atomic, Encoded>(sel, imp, cls); 47 return; 48 } 49 // 此位置已经存值,且 .sel 就是传来的这个 sel 了 50 if (b[i].sel() == sel) { 51 // The entry was added to the cache by some other thread 52 // before we grabbed the cacheUpdateLock. 53 return; 54 } 55 } while (fastpath((i = cache_next(i, m)) != begin));// 再次哈希 --> (i+1)&mask != begin 56 57 cache_t::bad_cache(receiver, (SEL)sel, cls); 58 }
cache_t::insert 逻辑流程概况图:
从代码逻辑流程中,我们可以得到上面问题答案:
1、occupied 从1->2->1->2->3 的原因:当 cache ≥ 3/4capacity 时,空间会重新开辟并释放旧的空间,同时 occupied 手动置0.
2、mask 值变化原因: mask = capacity - 1,所以 它的值是 3 7 15......
3、方法的缓存与调用顺序:缓存时通过哈希算法:sel & mask 对 sel 存放位置 index 计算的,so 缓存是乱序的。
4、buckets 中方法丢失:因 occupied>2 空间会被重新开辟,旧的空间会被释放free,之前的缓存的方法自然也会一起清掉。后面再掉的话会再次缓存。
do{}while() 的流程:
以上。
问题:cache_t::insert 什么时候调用呢?--> 方法调用流程 --> objc_msgSend 消息发送流后程续文章继续探索。