引言
我们在前面的文章006—iOS底层 - 类的结构(属性、成员变量、方法的探索)探索了类的结构,并探索了bits,本文我将讲述探索cache的过程。
###cache_t结构
与前面的文章006一样的方式,在objc源码中,搜索struct objc_class找到struct objc_class : objc_object,找到objc_class内部的cache_t cache。进入cache_t内部,知道其内部结构,成员变量的大小如下

  1. struct cache_t {
  2. private:
  3. explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8字节
  4. union {
  5. struct {
  6. explicit_atomic<mask_t> _maybeMask; // 4字节
  7. #if __LP64__
  8. uint16_t _flags; // 2字节
  9. #endif
  10. uint16_t _occupied; // 2字节
  11. };
  12. explicit_atomic<preopt_cache_t *> _originalPreoptCache;// 8字节
  13. };
  14. 以下代码省略...

1、#if __LP64__为架构判断,008--iOS底层 - 类的结构(cache上) - 图2

2、这些成员变量都是什么意思,分别代表什么?我们先通过lldb调试去看看里面的值。
3、我们前面探索bits的时候,用的是内存偏移0x20得到的,同理我们可以通过偏移0x10得到cache_t
###LLDB调试探索cache
话不多说,直接上操作过程。操作过程与探索bits的操作流程差不多,我们在打印cache的时候,发现里面的值也无法获取,这时想到的,应该是cache_t内部的取值函数。008--iOS底层 - 类的结构(cache上) - 图3
####bucket_t
我们打印了cache_t内容后,毫无收获,此时,应该在cache_t内部寻找对应的功能方法(函数)继续探索。在寻找过程中,发现bucket_t是出现频率最高的,说明bucket_t的重要性。下面我们将探索bucket_t的内部结构,简化后的源码如下:

  1. struct bucket_t {
  2. private:
  3. // IMP-first is better for arm64e ptrauth and no worse for arm64.
  4. // SEL-first is better for armv7* and i386 and x86_64.
  5. #if __arm64__
  6. explicit_atomic<uintptr_t> _imp;
  7. explicit_atomic<SEL> _sel;
  8. #else
  9. explicit_atomic<SEL> _sel;
  10. explicit_atomic<uintptr_t> _imp;
  11. #endif
  12. public:
  13. static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
  14. inline SEL sel() const { return _sel.load(memory_order_relaxed); }
  15. inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
  16. uintptr_t imp = _imp.load(memory_order_relaxed);
  17. if (!imp) return nil;
  18. #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
  19. SEL sel = _sel.load(memory_order_relaxed);
  20. return (IMP)
  21. ptrauth_auth_and_resign((const void *)imp,
  22. ptrauth_key_process_dependent_code,
  23. modifierForSEL(base, sel, cls),
  24. ptrauth_key_function_pointer, 0);
  25. #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
  26. return (IMP)(imp ^ (uintptr_t)cls);
  27. #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
  28. return (IMP)imp;
  29. #else
  30. #error Unknown method cache IMP encoding.
  31. #endif
  32. }
  33. ...
  34. void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
  35. };

当我们看到熟悉的_sel_imp知道bucket_t内部主要存放着_sel_imp,并提供了sel()imp()函数,给外界传值。
我们继续lldb调试,
QLPerson.h

  1. @interface QLPerson : NSObject
  2. @property (nonatomic,copy) NSString *nickName;
  3. @property (nonatomic,strong) NSString *strongNickName;
  4. - (void)test1;
  5. @end

main.m

  1. QLPerson *person = [QLPerson alloc];
  2. NSLog(@"%@",person);

lldb调试步骤如下:008--iOS底层 - 类的结构(cache上) - 图4008--iOS底层 - 类的结构(cache上) - 图5

1、buckets()返回的不只一个bucketbuckets是一种哈希链表,其内部元素是无序的,新的数据通过一定的算法,找到下标,将新数据插入。
2、在打印buckets内部数据的时候,取值跟数组一样,通过下标取值,一直找到_sel的value_imp的value有值的时候,调用sel(),imp()方法,找到person内的test1方法声明和实现。

脱离源码自定义cache
设计代码如下:
QLPerson.h中属性和方法声明如下:

  1. @interface QLPerson : NSObject
  2. - (void)test1;
  3. - (void)test2;
  4. - (void)test3;
  5. - (void)test4;
  6. - (void)test5;
  7. + (void)test12;
  8. @end

main.m中,通过提取objc源码中的struct bucket_tcache_tchass_data_bits_tobjc_class的部分关键代码,加入前缀,组成一个模拟objc_class结构的代码,用于对objc_class内部的数据变化情况:

  1. typedef uint32_t mask_t;
  2. struct ql_bucket_t {
  3. SEL _sel;
  4. IMP _imp;
  5. };
  6. struct ql_cache_t {
  7. struct ql_bucket_t *_buckets;
  8. mask_t _maybeMask;
  9. uint16_t _flags;
  10. uint16_t _occupied;
  11. };
  12. struct ql_class_data_bits_t {
  13. uintptr_t bits;
  14. };
  15. struct ql_objc_class {
  16. Class isa;
  17. Class superclass;
  18. struct ql_cache_t cache; // formerly cache pointer and vtable
  19. struct ql_class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
  20. };
  21. int main(int argc, const char * argv[]) {
  22. @autoreleasepool {
  23. QLPerson *person = [QLPerson alloc];
  24. Class pClass = [person class];
  25. [person test1];
  26. struct ql_objc_class *ql_class = (__bridge struct ql_objc_class *)(pClass);
  27. struct ql_cache_t cache = ql_class->cache;
  28. NSLog(@"_occupied:%u --- _maybeMask:%u",cache._occupied,cache._maybeMask);
  29. for (mask_t i = 0; i < cache._maybeMask; i++ ) {
  30. struct ql_bucket_t bucket = cache._buckets[i];
  31. NSLog(@"SEL:%@---IMP:%p",NSStringFromSelector(bucket._sel),bucket._imp);
  32. }
  33. }
  34. return 0;
  35. }

解析:
1、ql_bucket_t对应objc源码如下,我们在之前的文章提到过,泛型的最终类型为传入的具体类型,即此处的_sel类型为SEL_imp的类型为uintptr_t,而我们之前打印过imp的值,其实是一个地址,也就是说,按照我们的经验,我们直接讲_imp的类型定义为IMP

  1. #if __arm64__
  2. explicit_atomic<uintptr_t> _imp;
  3. explicit_atomic<SEL> _sel;
  4. #else
  5. explicit_atomic<SEL> _sel;
  6. explicit_atomic<uintptr_t> _imp;
  7. #endif

2、ql_cache_t对应objc源码中的cache_t

  1. struct cache_t {
  2. explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
  3. union {
  4. struct {
  5. explicit_atomic<mask_t> _maybeMask;
  6. #if __LP64__
  7. uint16_t _flags;
  8. #endif
  9. uint16_t _occupied;
  10. };
  11. explicit_atomic<preopt_cache_t *> _originalPreoptCache;
  12. };

1)_bucketsAndMaybeMask实际类型为uintptr_t
2)unionstruct_originalPreoptCache互斥,在__LP64__机器架构中,用到的是struct,又因为struct内部只有基本类型变量,则内存大小为内部成员变量相加后的内存平移得到的结果,因此cache_t的结构变成了我们上面设计的ql_cache_t,我们在探索_bucketsAndMaybeMask的作用是通过store()load()存取buckets。因此我们简化流程,将_bucketsAndMaybeMask直接替换成ql_bucket_t *指针,最终得到ql_cache_t的简化结构。
3、由于objc_class是继承于objc_object的,因此ql_objc_class应包含Class isa。若是不把isa加进来,则会出现以下结果:008--iOS底层 - 类的结构(cache上) - 图6
_maybeMask的值为无规律的数据,在此之前,我们打印过的_maybeMask的值都是切确的37这样子的数据。_maybeMask的错乱,说明了一个问题:指针混乱了。(我们将一个类转换成struct ql_objc_class,类中有isa在内的各种成员变量,若ql_objc_class中不加入isa,则将类的isa开始的结构完整的赋值到缺少isaql_objc_class,导致数据错乱),正确的结果如下:008--iOS底层 - 类的结构(cache上) - 图7

测试代码操作顺序
1、只保留test1方法调用,结果如下:_occupied:1 --- _maybeMask:3,并且得到test1imp

  1. 2021-07-05 22:19:34.680748+0800 QLObjcTest[46201:587343] -----[QLPerson test1]---
  2. 2021-07-05 22:19:34.681817+0800 QLObjcTest[46201:587343] _occupied1 --- _maybeMask3
  3. 2021-07-05 22:19:34.682090+0800 QLObjcTest[46201:587343] SEL:(null)---IMP:0x0
  4. 2021-07-05 22:19:34.682201+0800 QLObjcTest[46201:587343] SEL:(null)---IMP:0x0
  5. 2021-07-05 22:19:34.682628+0800 QLObjcTest[46201:587343] SEL:test1---IMP:0xb298

2、保留test1test2方法调用,结果如下:_occupied:2 --- _maybeMask:3,并且得到test1、test2imp

  1. 2021-07-05 22:21:03.659769+0800 QLObjcTest[46225:588419] -----[QLPerson test1]---
  2. 2021-07-05 22:21:03.660588+0800 QLObjcTest[46225:588419] -----[QLPerson test2]---
  3. 2021-07-05 22:21:03.660962+0800 QLObjcTest[46225:588419] _occupied2 --- _maybeMask3
  4. 2021-07-05 22:21:03.661402+0800 QLObjcTest[46225:588419] SEL:test2---IMP:0xb250
  5. 2021-07-05 22:21:03.661502+0800 QLObjcTest[46225:588419] SEL:(null)---IMP:0x0
  6. 2021-07-05 22:21:03.661771+0800 QLObjcTest[46225:588419] SEL:test1---IMP:0xb280

3、test1test2test3方法调用,结果如下:_occupied:1 --- _maybeMask:7,但只有test3imp

  1. 2021-07-05 22:21:43.370104+0800 QLObjcTest[46243:589189] -----[QLPerson test1]---
  2. 2021-07-05 22:21:43.370911+0800 QLObjcTest[46243:589189] -----[QLPerson test2]---
  3. 2021-07-05 22:21:43.371076+0800 QLObjcTest[46243:589189] -----[QLPerson test3]---
  4. 2021-07-05 22:21:43.371333+0800 QLObjcTest[46243:589189] _occupied1 --- _maybeMask7
  5. 2021-07-05 22:21:43.371431+0800 QLObjcTest[46243:589189] SEL:(null)---IMP:0x0
  6. 2021-07-05 22:21:43.371535+0800 QLObjcTest[46243:589189] SEL:(null)---IMP:0x0
  7. 2021-07-05 22:21:43.371634+0800 QLObjcTest[46243:589189] SEL:(null)---IMP:0x0
  8. 2021-07-05 22:21:43.371698+0800 QLObjcTest[46243:589189] SEL:(null)---IMP:0x0
  9. 2021-07-05 22:21:43.371746+0800 QLObjcTest[46243:589189] SEL:(null)---IMP:0x0
  10. 2021-07-05 22:21:43.371792+0800 QLObjcTest[46243:589189] SEL:(null)---IMP:0x0
  11. 2021-07-05 22:21:43.372053+0800 QLObjcTest[46243:589189] SEL:test3---IMP:0xb228

4、test1test2test3test4方法调用,结果如下:_occupied:2 --- _maybeMask:7,但只有test3、test4imp

  1. 2021-07-05 22:24:43.640023+0800 QLObjcTest[46279:591066] -----[QLPerson test1]---
  2. 2021-07-05 22:24:43.640809+0800 QLObjcTest[46279:591066] -----[QLPerson test2]---
  3. 2021-07-05 22:24:43.641000+0800 QLObjcTest[46279:591066] -----[QLPerson test3]---
  4. 2021-07-05 22:24:43.641100+0800 QLObjcTest[46279:591066] -----[QLPerson test4]---
  5. 2021-07-05 22:24:43.641524+0800 QLObjcTest[46279:591066] _occupied2 --- _maybeMask7
  6. 2021-07-05 22:24:43.641648+0800 QLObjcTest[46279:591066] SEL:(null)---IMP:0x0
  7. 2021-07-05 22:24:43.641747+0800 QLObjcTest[46279:591066] SEL:(null)---IMP:0x0
  8. 2021-07-05 22:24:43.641836+0800 QLObjcTest[46279:591066] SEL:(null)---IMP:0x0
  9. 2021-07-05 22:24:43.641936+0800 QLObjcTest[46279:591066] SEL:(null)---IMP:0x0
  10. 2021-07-05 22:24:43.642308+0800 QLObjcTest[46279:591066] SEL:test4---IMP:0xbe00
  11. 2021-07-05 22:24:43.642380+0800 QLObjcTest[46279:591066] SEL:(null)---IMP:0x0
  12. 2021-07-05 22:24:43.642548+0800 QLObjcTest[46279:591066] SEL:test3---IMP:0xb1d0

5、test1test2test3test4test5方法调用,结果如下:_occupied:3 --- _maybeMask:7,但只有test3、test4、test5imp

  1. 2021-07-05 22:25:41.883301+0800 QLObjcTest[46303:591991] -----[QLPerson test1]---
  2. 2021-07-05 22:25:41.884102+0800 QLObjcTest[46303:591991] -----[QLPerson test2]---
  3. 2021-07-05 22:25:41.884218+0800 QLObjcTest[46303:591991] -----[QLPerson test3]---
  4. 2021-07-05 22:25:41.884274+0800 QLObjcTest[46303:591991] -----[QLPerson test4]---
  5. 2021-07-05 22:25:41.884324+0800 QLObjcTest[46303:591991] -----[QLPerson test5]---
  6. 2021-07-05 22:25:41.884578+0800 QLObjcTest[46303:591991] _occupied3 --- _maybeMask7
  7. 2021-07-05 22:25:41.884670+0800 QLObjcTest[46303:591991] SEL:(null)---IMP:0x0
  8. 2021-07-05 22:25:41.884723+0800 QLObjcTest[46303:591991] SEL:(null)---IMP:0x0
  9. 2021-07-05 22:25:41.884972+0800 QLObjcTest[46303:591991] SEL:test5---IMP:0xbe38
  10. 2021-07-05 22:25:41.885029+0800 QLObjcTest[46303:591991] SEL:(null)---IMP:0x0
  11. 2021-07-05 22:25:41.885180+0800 QLObjcTest[46303:591991] SEL:test4---IMP:0xbe08
  12. 2021-07-05 22:25:41.885232+0800 QLObjcTest[46303:591991] SEL:(null)---IMP:0x0
  13. 2021-07-05 22:25:41.885286+0800 QLObjcTest[46303:591991] SEL:test3---IMP:0xb1d8

省略号………….
由此规律,我们操作到test8的时候,两者的值为_occupied:1 --- _maybeMask:15,打印的selimp只有test8有值,其他都是null,为什么会这样呢?猜想,当类中的方法达到某个数量的时候,_occupiedmaybeMask都会随着数量的变化而变化,那他们分别代表什么意思?

总结
本文主要讲述探究cache_t内部结构,并通过lldb调试和脱离objc源码环境模拟objc_class内部的情况,探索cache_tbucket_t_occupied_maybeMask的数值,看到cache_t内的变化规律。下一篇文章将探索cache_t的流程。
1、通过上面的探索,用图来表示008--iOS底层 - 类的结构(cache上) - 图8
2、解释:
sel:方法编号,相当于一本书的某一条目录
imp:函数指针地址,相当于该条目录对应的页码。通过此页码得到具体内容