iOS类的方法缓存

116 阅读4分钟

在结构体objc_class中,superclass和bits我之前已经讲过了,今天来研究下cache,看看里面究竟有什么。

    // 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

进入cache_t结构体,发现有个insert()的方法,它的作用就是往内存中插入方法。

图片.png

进入insert方法:

图片.png

其中,bucket_t结构体b可以理解为一个数组,bucket理解成一个桶子,这个桶子里面可以存多个方法,每个方法包含sel和imp。

进入do...while里面的set方法:

图片.png

这里的store方法,就是将方法放到内存中,这是set方法的主要目的。

要插入一个方法,需要先确定要将其插入到什么位置。insert方法里,插入的位置是通过hash算法来确定的:
mask_t begin = cache_hash(sel, m);
mask_t m = capacity - 1中的capacity,是通过这几个方法确定的:

图片.png 图片.png 图片.png

mask方法读取了_maybeMask的值,为buckets的长度-1,capacity()方法正好返回了mask()+1,所以最终的m的值就是buckets的长度,即桶子(buckets)的长度

找到了函数应该被插入的位置i后,do...while里面的逻辑大致就是判断这个位置里存放的信息:
1、如果没有存任何缓存方法,就将该方法存于此处;
2、如果已经存放且正好是要被插入的这个方法,那就什么也不做;
如果i处的情况不符合上述中的任何一种,比如i处存放了别的方法,那么i的位置将往下移动1,然后继续做上述的判断。

我们先来根据下面的代码,查看下缓存里的方法:

图片.png

图片.png

图片.png

奇怪了,class方法和respondsToSelector方法,我并没有调用啊,怎么会存了这两个方法呢?

看这里,可知这是LLDB调试时自动调用的方法。

图片.png

我们让对象调一个方法,看看这个方法会不会被存到缓存。通过同样的方法查看缓存里的方法,这一次只找到了class方法,respondsToSelector方法不见了。这是什么原因呢?这是因为缓存做了扩容

我们看下insert方法里的这段代码:

图片.png

这里的逻辑是这样的:

①、如果缓存为空,给缓存设置一个容量,其初始值为INIT_CACHE_SIZE,这个大小在x86_64架构下为4,arm64架构下为2。容量就表示桶子的长度。

②、在arm64架构下,算上即将要存储的这个方法,如果缓存的大小<=桶子长度的7/8,则什么也不做;在x86_64架构下,算上即将要存储的这个方法,如果缓存的大小+1<=桶子长度的3/4,则什么也不做;代码中的cache_fill_ratio(capacity),在x86_64架构下,为capacity * 3/4;在arm64架构下,为capacity * 7/8

③、在arm64架构下,当桶子的长度<=8桶子还没存满时,就什么也不用做;

④、这里就需要扩容了,容量为原来的两倍,能扩容到的最大值为2的16次方。扩容时,在reallocate方法最后一个参数为true,就会为了删除原来的老桶子。以x86_64架构为例,假如现在桶子的长度为4,当要存储第三个方法的时候,就需要扩容了,会重新创建一个长度为8的新桶子,把这次要存储的这个方法存进新桶子,这时候还要把老桶子释放掉,那么在此之前已经存的两个方法,也就随着老桶子一起被释放了。

刚才我们对象p调用的方法method1没有在缓存中查到,加上之前的class和respondsToSelector,一共三个方法,我们只在缓存里面找到了一个class,这就是因为扩容,把method1和respondsToSelector所在的那个老桶子释放了,而且method1方法是在class和respondsToSelector之前调用的。

我们多调几个方法试一下!

图片.png

图片.png

再多调用了两个方法以后,我们发现除了method1和method2以外,method3、class和respondsToSelector都在cache里面找到了,这是因为method1和method2调用的早,被缓存的也早,而之后cache扩容时,它们随着老桶子释放了。

至此,一个类缓存方法的逻辑我们基本就明了了,以前看到很多人说cache里存的是“这个类最近调用过的方法”,现在看来,这样说不是太准确,cache扩容一次就把之前缓存的方法都删掉了。