我们在探索类对象的底层源码的时候,在类对象的结构体中object_class遗留了一个cache_t结构体类型的cache的成员变量。今天我们就来探索这个关于cache的底层结构。我们在之前对bits的探索中,是相对类对象的地址平移32个字节,那么cache就是平移16个字节,我们按照之前的方法对其内部结构进行探索,得到如下结果:
cache_t中有什么?
但是貌似并没有我们需要的东西,所以我们点开cache_t结构体的定义,我们在其中查找方法,既然是缓存,那我们就需要向其中插入数据。所以我们找到了一个void insert(SEL sel, IMP imp, id receiver);的方法,查看其定义,我们找其核心内容,发现在do...while...的循环中,是对一个bucket_t进行set操作:
其中的两个参数
Automic和Encoded的值为:
接着我们查看这个
bucket_t的结构体:
再查看这个
set方法的实现:
所以这个
set方法整体的意思就是向bucket_t的结构体中写入sel和imp。
接着我们再回到insert方法中,我们看到它是通过下标的方式向bucket_t中插入数据的,而这个下标i的值来自于begin,begin是通过cache_hash的方法传入sel和m两个参数,sel我们知道是方法名,m来自于capacity-1,而这个capacity来自于oldCapacity,oldCapacity来自于capacity()方法调用:
接着我们查看
capacity这个函数的实现:
看到其中有一个叫做
mask的函数,我们接着查看它的实现:
我们看到
mask在读取_maybeMask的值,这里我们预先知道这个_maybeMask代表bucket_t的长度-1,capacity代表bucket_t的长度,我们后面会说到。
我们接着再来看cache_hash方法的实现:
我们发现它
return了value与上mask的值,value表示sel的unsigned long转换后的值,一定会是个很大的值,而mask是函数传入的值,前面我们知道它传入的是capacity-1,也就是
bucket_t的长度。我们知道一个很大的数字与上一个很小的数字,得到的结果不会超过最小的数字。
我们在来看这个do...while,我们把b就看成一个数组,里面存储着bucket_t类型。这个i值是通过hash得到,其中的第一个判断表示如果如果当前的bucket_t的sel中没有值,就会向里面写入。第二个表示如果当前的bucket_t的sel与传入的sel相等,表示已经缓存过了,则return掉。while中会对i做进一步的处理,这个cache_next是对这个i进行增大。
通过以上的分析,我们了解到这个cache是对方法的缓存(sel和imp)。
cache_t的扩容
既然我们知道了cache_t的结构体中有一个插入bucket_t的insert方法,那我们继续找,就找到了一个buckets的get方法,前面我们已经拿到了关于cache_t的结构体地址,那我们就可以通过该地址对buckets方法进行调用,得到其中的内容:
我们知道
sel和imp是存放在一个装有bucket_t结构体类型的数组中的,那我们既然拿到了其首地址就能通过内存平移的方式拿到其他的,如图是在对象没有调用任何方法的前提下得到的结果,数组中装了class和respondsToSelector的方法,在没有显式调用的情况下这两个方法是如何被调用的呢,我们后面会说到。接下来我们调用其中的对象方法,接着探索cache_t中的内容:
我们调用了方法之后,为啥cache_t中的内容只剩下一个
class了呢。
cache_t的大小
我们回到insert方法的实现,探索如下内容:
第一个判断:
如上可得:在
arm64下,系统会开辟一个长度为2的桶子,在x86_64下是4。
第二个判断,我们查看cache_fill_ratio的定义:
也就是说在
arm64架构下如果缓存的大小小于等于桶子的长度的八分之七或者在x86_64架构下如果缓存的大小小于等于桶子长度的四分之三,就什么也不做。
第三个判断,表示在arm64架构下,当桶子的长度小于等于8的时候,什么也不做。
当前面的判断走完后,会进入else分支,进行两倍扩容,如果长度大于MAX_CACHE_SIZE,则设置其大小为极限值MAX_CACHE_SIZE。reallocate会释放旧桶。
所以我们得出cache的扩容规则为:在x86_64架构下,当缓存的大小大于等于桶子长度的四分之三的时候进行两倍扩容;在arm64架构下,当当前缓存的方法数+1的大小大于等于长度的八分之七的时候进行两倍扩容,当桶子的长度小于等于8并且目前缓存的大小+1小于等于桶子的大小,不会扩容(桶子小于8的时候存满了才扩容)。
所以我们就明白了为什么之前调用了方法之后会什么没有找到,arm64下,初始值为2,当第1个方法缓存的时候,则要进行两倍扩容为4,并且需要清除旧桶。所以instanceMethod在刚进来扩容的时候就被清除掉了,也就找不到了。而前面我们说到class方法和responseToSelector方法是我们在调试的时候通过lldb调用产生的。
那cache_t中缓存方法的多少会对其大小有影响么?不会的,因为它的成员变量_bucketsAndMaybeMask就是buckets的首地址。