类的结构分析 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’ 的字段 –> 掩码、指针平移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
10 ASSERT(sel != 0 && cls->isInitialized());
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 }
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;
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
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 消息发送流后程续文章继续探索。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!