OC 底层探索 07、类的结构分析2

本文来探索类结构中 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 消息发送流后程续文章继续探索。

原文地址:https://www.cnblogs.com/zhangzhang-y/p/13704591.html