objc_class中cache_t的分析

526 阅读3分钟

前言

前3篇文章简单的探索了class_data_bits_t,现在开始cache_t的探究。 什么是cache_t?作用是什么?

cache_t缓存,为了限制对频繁访问方法定义的方法列表进行线性搜索的需要(这种操作会大大降低方法查找的速度),Objective-C运行时函数在objc_cache数据结构中存储指向类中最近调用方法定义的指针,源自官方文档

struct objc_cache { 
    unsigned int mask; 
    unsigned int occupied; 
    Method buckets[1];
};

解释:

  • mask:一个整数,指定已分配的缓存桶总数(减1)。在方法查找期间,Objective-C运行时使用该字段确定开始桶数组线性搜索的索引。指向方法选择器的指针使用逻辑与操作(index = (mask & selector))对该字段进行屏蔽。这是一个简单的哈希算法。
  • occupied:表示已使用的缓存桶总数。
  • buckets:指向方法数据结构的指针数组。这个数组不能包含超过掩码+ 1的项。注意,指针可以是NULL,表示缓存桶未被占用,并且被占用的桶可能不是连续的。这个数组可能会随着时间的推移而增长。 以上都是官方对cache_t的解释,看完这些定义我们来自己研究。

一、cache_t的结构

打开源码,首先我们找到objc_class,如图: 9B389CEE-B598-4BE3-89FB-8D1776C3AFCF.png objc_class的一个成员变量cache,类型是cache_t,这就是我们的研究对象。在前面的探究中我们知道isasuperclass都是指针8位,所以首地址平移16位就是cache,这里记下以便后面的探究。

1、源码中的cache_t

从上面的源码中我们可以跳到cache_t,如图: 36475545-F876-45AD-8CC6-3F3A20F99D7B.png 为了方便阅读我们省略了一些宏判断,如图所示,圈起来的方法是我们获取相关变量的方法,mask()occupied()的返回值都是mask_t,mask_t的值如下所示:

#if __LP64__

typedef uint32_t mask_t// x86_64 & arm64 asm are less efficient with 16-bits

#else

typedef uint16_t mask_t;

#endif

typedef uintptr_t SEL;

所以我们应该关注bucket_t,如下图所示: 9FDEAD5D-752D-49E3-959E-733682945536.png 在这里我们发现了_sel_imp,还有2个相关的获取方法。

2、lldb验证源码

首先我们创建LGPerson,声明: 截屏2021-08-05 下午9.40.26.png 实现: 截屏2021-08-05 下午9.40.38.png 然后使用并断点: 71D3FAEF-63A0-4265-8622-7B3D163D97EB.png 接下来断点调试: 1971AB7F-50D8-423F-95E8-BCA025F8E77C.png 这里我们找到了maskoccupied分别为三,表示有3个缓存位置,其中1个存了数据。接下来我们找具体的方法,如图: EDCB0F3D-40C3-43F2-AC8C-7D7A2836E159.png 我们发现只有第2号位置有缓存,第0和第1号位置为空,跟前面maskoccupied的值一样,接下来打印查看具体的selimp,如图: BF55294C-A6DB-4A11-BBE5-70288E875F52.png 成功打印出了sayWork方法的selimp

3、小结

cache_t中缓存存方法的bucket_t(桶子),桶子中存放在selimp

二、脱离源码调试

lldb调试有太多的限制,如需要配置好的源码、每次调试都需要从头开始等。如果可以脱离源码就可以任意调试了,那么如何脱离源码呢?拿我们上面cache_t的调试为例子,我们可以将需要的结构体换成我们自定义的,做到结构相同就可以了,所以试着写一些我们自己的objc_calssclass_data_bits_tcache_tbucket_t。如图: 截屏2021-08-05 下午10.45.22.png 然后调用: 截屏2021-08-05 下午10.46.27.png 运行: 截屏2021-08-05 下午10.51.05.png 这就打印出来了,是不是跟上面的lldb一样?这样就不用一次次的一步一步的查找了。一次构建,终身受益。