在前面几篇文章中 我们分别探索了 objc_class 中的 isa , superClass , bits. 现在我们来看看 cache_t 中到底有什么作用
一 . cache_t 的结构
在这段类结构代码中,我们可以看到 类结构中存在一个cache_t
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct objc_class : objc_object {
// Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() const { |
下面是部分cache_t的结构
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 |
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED // macOS 模拟器
explicit_atomic<struct bucket_t *> _buckets; explicit_atomic<mask_t> _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. // Additional bits after the mask which must be zero. msgSend // The largest mask value we can store. 可以存储的最大掩码值 2^16-1 // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer. static constexpr uintptr_t maskBits = 4; #if __LP64__ |
由于 主要我们实在 Mac 上调试, 所以最终的 cache_t 的结构 具有四个参数
1
2 3 |
explicit_atomic<struct bucket_t *> _buckets; // 存储方法的 hash 表
explicit_atomic<mask_t> _mask; // 算法使用的掩码 uint16_t _occupied; // hash 表中占用的数量 |
其中主要是看 bucket_t 的结构
我们可以看到 真机和 模拟器的区别就是。 imp 和 sel 的顺序问题
看注释我们可以知道 这样调整顺序是在相应的架构下 有更好的优化
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 |
struct bucket_t {
private: // IMP-first is better for arm64e ptrauth and no worse for arm64. // SEL-first is better for armv7* and i386 and x86_64. #if __arm64__ // 真机 explicit_atomic<uintptr_t> _imp; // 函数指针,指向方法的具体实现 explicit_atomic<SEL> _sel; // 方法编号 #else // 模拟器 explicit_atomic<SEL> _sel; explicit_atomic<uintptr_t> _imp; #endif // Compute the ptrauth signing modifier from &_imp, newSel, and cls. // Sign newImp, with &_imp, newSel, and cls as modifiers. |
二. cache_t 方法缓存的添加
- LLDB 调试准备 我们先建立一个测试类,其中添加一些实例方法
eg.
1
2 3 4 |
– (void)logCup;
– (void)logPen; – (void)logKeyboard; – (void)logMouse; |
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
int main(int argc, const char * argv[]) {
@autoreleasepool { // insert code here… TObject *t = [TObject alloc]; NSLog(@”%p”, t); [t logCup]; } |
- 我们在初始化之前先下个断点 进行 lldb 调试 ,由下面的调试我们可以知道 缓存 内都是空的,并没有任何方法的痕迹
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
(lldb) p/x TObject.class // 获取 class 类对象地址
(Class) $0 = 0x0000000100002318 TObject (lldb) p (cache_t*)0x0000000100002328 // 通过地址偏移16字节,强转 cache_t 类型 (cache_t *) $1 = 0x0000000100002328 (lldb) p *$1 (cache_t) $2 = { _buckets = { std::__1::atomic<bucket_t *> = 0x000000010032d420 { _sel = { std::__1::atomic<objc_selector *> = (null) } _imp = { std::__1::atomic<unsigned long> = 0 } } } _mask = { std::__1::atomic<unsigned int> = 0 } _flags = 32804 _occupied = 0 } |
- 接下来 我们在调用 alloc 之后下一个断点 继续调试
我们可以看到 _buckets 缓存数据发生了改变
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
(lldb) p/x t.class
(Class) $6 = 0x0000000100002318 TObject (lldb) p (cache_t*) 0x0000000100002328 (cache_t *) $7 = 0x0000000100002328 (lldb) p *$7 (cache_t) $8 = { _buckets = { std::__1::atomic<bucket_t *> = 0x0000000101004bf0 { _sel = { std::__1::atomic<objc_selector *> = “” } _imp = { std::__1::atomic<unsigned long> = 3274184 } } } _mask = { std::__1::atomic<unsigned int> = 3 } _flags = 32804 _occupied = 2 } (lldb) |
- 我们在每个方法都打个断点,当调用完第一个方法之后我们进行调试, 我们看到 _occupied 又变成 1 了,那这到底做了什么呢?
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
2020-12-21 23:54:11.872707+0800 Objc_m [2835:50950] -[TObject logCup]
(lldb) p *$1 (cache_t) $3 = { _buckets = { std::__1::atomic<bucket_t *> = 0x00000001007117a0 { _sel = { std::__1::atomic<objc_selector *> = (null) } _imp = { std::__1::atomic<unsigned long> = 0 } } } _mask = { std::__1::atomic<unsigned int> = 7 } _flags = 32804 _occupied = 1 } |
- 我们在第二个方法调用完下个断点,继续调试,我们可以看到 _occupied 又加了1
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
2020-12-21 23:56:51.210222+0800 Objc_m[2835:50950] -[TObject logPen]
(lldb) p *$1 (cache_t) $4 = { _buckets = { std::__1::atomic<bucket_t *> = 0x00000001007117a0 { _sel = { std::__1::atomic<objc_selector *> = (null) } _imp = { std::__1::atomic<unsigned long> = 0 } } } _mask = { std::__1::atomic<unsigned int> = 7 } _flags = 32804 _occupied = 2 } |
- 我们在第三个方法调用完下个断点,继续调试,我们可以看到 _occupied 又加了1, 这是我们可以猜测 _occupied 和方法的个数有关
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
2020-12-21 23:59:32.413929+0800 Objc_m[2835:50950] -[TObject logMouse]
(lldb) p *$1 (cache_t) $5 = { _buckets = { std::__1::atomic<bucket_t *> = 0x00000001007117a0 { _sel = { std::__1::atomic<objc_selector *> = “” } _imp = { std::__1::atomic<unsigned long> = 12264 } } } _mask = { std::__1::atomic<unsigned int> = 7 } _flags = 32804 _occupied = 3 } |
此时我们就可以通过 源码进行调试,搜索_occupied,观察其变化,也就有了下面的代码
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
void cache_t::incrementOccupied()
{ _occupied++; } void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver) ASSERT(sel != 0 && cls->isInitialized()); // Use the cache as-is if it is less than 3/4 full bucket_t *b = buckets(); // Scan for the first unused slot and insert there. cache_t::bad_cache(receiver, (SEL)sel, cls); #if !DEBUG_TASK_THREADS |
我们下断点 观察一下 alloc 到底做了什么
可以看到 调用了 alloc 方法, 接着 开辟了4 字节的空间,接着调用 return 方法,添加引用技术,所以 _occupied 变成了2
通过断点 我们发现 之前 的 2 都是针对于 NSObject 类对象产生的
而之后的方法的 Tobject 调用方法 使用的是 Tobject 的类对象中的 cache_t
三. 总结
- OC 中的实例方法混存在类中,类方法缓存在元类上
- 缓存最小是4字节,缓存充满 3/4时,会扩容至当前的2倍
- 扩容并不是在原来的基础上添加,而是重新开辟新的 allocateBuckets,释放旧的 cache_collect_free,把最近一次临界的imp和key缓存进来,经典的LRU算法。
转载请注明:XAMPP中文组官网 » OC底层原理实现 cache_t 结构方法缓存