Cache方法缓存

162 阅读5分钟

我们在探索类对象的底层源码的时候,在类对象的结构体中object_class遗留了一个cache_t结构体类型的cache的成员变量。今天我们就来探索这个关于cache的底层结构。我们在之前对bits的探索中,是相对类对象的地址平移32个字节,那么cache就是平移16个字节,我们按照之前的方法对其内部结构进行探索,得到如下结果:

截屏2022-05-04 上午9.36.58.png

cache_t中有什么?

但是貌似并没有我们需要的东西,所以我们点开cache_t结构体的定义,我们在其中查找方法,既然是缓存,那我们就需要向其中插入数据。所以我们找到了一个void insert(SEL sel, IMP imp, id receiver);的方法,查看其定义,我们找其核心内容,发现在do...while...的循环中,是对一个bucket_t进行set操作: 截屏2022-05-04 上午10.21.34.png 其中的两个参数AutomicEncoded的值为: 截屏2022-05-04 上午10.23.06.png 接着我们查看这个bucket_t的结构体: iShot2022-05-04_10.43.28.png 再查看这个set方法的实现: 截屏2022-05-04 上午10.32.03.png 所以这个set方法整体的意思就是向bucket_t的结构体中写入selimp

接着我们再回到insert方法中,我们看到它是通过下标的方式向bucket_t中插入数据的,而这个下标i的值来自于beginbegin是通过cache_hash的方法传入selm两个参数,sel我们知道是方法名,m来自于capacity-1,而这个capacity来自于oldCapacityoldCapacity来自于capacity()方法调用: 截屏2022-05-04 上午11.09.33.png 接着我们查看capacity这个函数的实现: 截屏2022-05-04 上午11.12.57.png 看到其中有一个叫做mask的函数,我们接着查看它的实现: 截屏2022-05-04 上午11.14.10.png 我们看到mask在读取_maybeMask的值,这里我们预先知道这个_maybeMask代表bucket_t的长度-1,capacity代表bucket_t的长度,我们后面会说到。

我们接着再来看cache_hash方法的实现: 截屏2022-05-04 上午11.22.02.png 我们发现它returnvalue与上mask的值,value表示selunsigned long转换后的值,一定会是个很大的值,而mask是函数传入的值,前面我们知道它传入的是capacity-1,也就是 bucket_t的长度。我们知道一个很大的数字与上一个很小的数字,得到的结果不会超过最小的数字。

我们在来看这个do...while,我们把b就看成一个数组,里面存储着bucket_t类型。这个i值是通过hash得到,其中的第一个判断表示如果如果当前的bucket_tsel中没有值,就会向里面写入。第二个表示如果当前的bucket_tsel与传入的sel相等,表示已经缓存过了,则return掉。while中会对i做进一步的处理,这个cache_next是对这个i进行增大。

通过以上的分析,我们了解到这个cache是对方法的缓存(selimp)。

cache_t的扩容

既然我们知道了cache_t的结构体中有一个插入bucket_tinsert方法,那我们继续找,就找到了一个buckets的get方法,前面我们已经拿到了关于cache_t的结构体地址,那我们就可以通过该地址对buckets方法进行调用,得到其中的内容: iShot2022-05-04_14.41.22.png 我们知道selimp是存放在一个装有bucket_t结构体类型的数组中的,那我们既然拿到了其首地址就能通过内存平移的方式拿到其他的,如图是在对象没有调用任何方法的前提下得到的结果,数组中装了classrespondsToSelector的方法,在没有显式调用的情况下这两个方法是如何被调用的呢,我们后面会说到。接下来我们调用其中的对象方法,接着探索cache_t中的内容: iShot2022-05-04_14.57.50.png 我们调用了方法之后,为啥cache_t中的内容只剩下一个class了呢。

cache_t的大小 截屏2022-05-04 下午3.11.44.png

我们回到insert方法的实现,探索如下内容:

截屏2022-05-04 下午4.11.02.png 第一个判断: 截屏2022-05-04 下午4.23.42.png 如上可得:在arm64下,系统会开辟一个长度为2的桶子,在x86_64下是4。 第二个判断,我们查看cache_fill_ratio的定义: 截屏2022-05-04 下午4.33.53.png 也就是说在arm64架构下如果缓存的大小小于等于桶子的长度的八分之七或者在x86_64架构下如果缓存的大小小于等于桶子长度的四分之三,就什么也不做。 第三个判断,表示在arm64架构下,当桶子的长度小于等于8的时候,什么也不做。

当前面的判断走完后,会进入else分支,进行两倍扩容,如果长度大于MAX_CACHE_SIZE,则设置其大小为极限值MAX_CACHE_SIZEreallocate会释放旧桶。

所以我们得出cache的扩容规则为:在x86_64架构下,当缓存的大小大于等于桶子长度的四分之三的时候进行两倍扩容;在arm64架构下,当当前缓存的方法数+1的大小大于等于长度的八分之七的时候进行两倍扩容,当桶子的长度小于等于8并且目前缓存的大小+1小于等于桶子的大小,不会扩容(桶子小于8的时候存满了才扩容)。

所以我们就明白了为什么之前调用了方法之后会什么没有找到,arm64下,初始值为2,当第1个方法缓存的时候,则要进行两倍扩容为4,并且需要清除旧桶。所以instanceMethod在刚进来扩容的时候就被清除掉了,也就找不到了。而前面我们说到class方法和responseToSelector方法是我们在调试的时候通过lldb调用产生的。

那cache_t中缓存方法的多少会对其大小有影响么?不会的,因为它的成员变量_bucketsAndMaybeMask就是buckets的首地址。