在objc_class结构体中,有一个cache_t类型,在这篇内容中,主要探索这个内容存储的数据,以及存储的规律。老样子通过内存位移先得到cache_t的内存地址。
我猜测插入方法,应该就是加入相应的方法缓存,因为有sel和imp,我们可以根据这个insert方法进行追踪,点进去看实现找到了一个buckts,方法内部就是操作这个buckts。
而cache_t有一个buckets()方法,进行打印
老样子,暂时看不出什么有用的信息,就去bucke_t结构体里面去瞧瞧
看一下这个capacity
unsigned oldCapacity = capacity(), capacity = oldCapacity;
继续跳下去
那么_maybeMask = capacity - 1
那我们就继续打印一下buckts方法,看看里面的内容。
继续打印
老规矩,看到不太明白的数据直接去源码中查找有没有对应的方法
我们可以通过查看sel方法来得到当前bucket_t指针数组中的存储内容,因为是一个指针数组类型,所以我们可以通过地址+1的方法,获取所有的存储内容.因为是散列表存储的,所以可能不连续,所以第一个+1的地址没有存储方法。
可以通过打印结果来看,bucket里面存储了一个class方法和一个respondsToSelector方法,既然是缓存方法,那么再调用方法之后,应该也可以打印出来,调用method1。
从图片来看打印结果,method1不仅没有出现,respondsToSelector也消失不见了。这就是因为缓存扩容的原因,旧的bucket被释放了,储存的数据也释放了。我们来探究一下cache扩容。
cache扩容
在insert方法中,如果缓存超出一定的临界值,就会进行扩容。
可以看出返回的是cache_t的成员变量_occupied,那么首次,_occupied=0,即newOccupied = 1;
//capacity方法实现
unsigned cache_t::capacity() const
{
return mask() ? mask()+1 : 0;
}
//mask方法实现
mask_t cache_t::mask() const
{//_maybeMask = bucket_t的长度-1
return _maybeMask.load(memory_order_relaxed);
}
oldCapacity = capacity();capacity = _maybeMask + 1;而 _maybeMask = bucket长度- 1;即capacity = bucket长度。
- 反过来看第一个条件判断
if (slowpath(isConstantEmptyCache())) {//当缓存为空的时候
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;//赋一个INIT_CACHE_SIZE
reallocate(oldCapacity, capacity, /* freeOld */false);
}
//跳一下 INIT_CACHE_SIZE
enum {
#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
// When we have a cache end marker it fills a bucket slot, so having a
// initial cache size of 2 buckets would not be efficient when one of the
// slots is always filled with the end marker. So start with a cache size
// 4 buckets.
INIT_CACHE_SIZE_LOG2 = 2,//
#else
// Allow an initial bucket size of 2 buckets, since a large number of
// classes, especially metaclasses, have very few imps, and we support
// the ability to fill 100% of the cache before resizing.
INIT_CACHE_SIZE_LOG2 = 1,
#endif
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
MAX_CACHE_SIZE_LOG2 = 16,
MAX_CACHE_SIZE = (1 << MAX_CACHE_SIZE_LOG2),
FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
};
//
MAX_CACHE_SIZE_LOG2的值依赖于CACHE_END_MARKER,而CACHE_END_MARKER:在x86_64下位1,在arm64下值为0,所以MAX_CACHE_SIZE_LOG2在x86_64下位2,在arm64下为1
INIT_CACHE_SIZE在x86_64下1<<2 = 4;在arm64下1<<1为2,即bucket_t初始化的长度,在arm64家沟下位2,在x86_64下为4;初始长度计算结束后,然后调用了reallocate(oldCapacity, capacity, false); //false是是否释放掉旧的bucket
reallocate实现
ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
collect_free(oldBuckets, oldCapacity);//第一次没有旧的buckets,所以不用释放
}
}
那么这个条件判断,我们就可以理解为在arm64架构下开辟一个长度为2的bucket,在x86_64架构下开辟一个长度为4的bucket。
- 再看第二个条件判断
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
//cache_fill_ratio方法如下图
由图可得,在x86_64架构下为bucket长度的四分之三,而CACHE_END_MARKER在x86_64下=1,计算基数会+1,即长度为4的情况下,第三个方法就会存进新的筒子里;
在arm64架构下为bucket长度的八分之七,即当长度为16的时候,第十五个数据会进行扩容。
也就是这个条件判断可以理解为,在arm64架构下,如果缓存的大小小于等于bucket长度的八分之七,在x86_64架构下如果缓存的大小小于等于bucket长度的四分之三,则什么也不干。
- 第三个条件
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
FULL_UTILIZATION_CACHE_SIZE 等于1<<3=8
即在arm64架构下,当筒子的长度小于等于8的时候,什么都不做
- 最后扩容写法
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
上述条件都不满足的时候,直接两倍扩容,最大值是MAX_CACHE_SIZE = 1<<16,最后reallocate传的是true,就是将旧bucket释放掉,原来在旧bucket存的值就不存在了
整个cache扩容的规则
- 在x86_64架构下,当缓存的大小等于bucket长度的四分之三的时候(会进行+1),进行两倍扩容
- 在arm64架构下,当缓存的大小小于等于bucket长度的半分之七的时候,进行两倍扩容,当bucket长度小于等于8的时候,不满不扩容
- 扩容后,释放旧bucket,新bucket不存储旧bucket内容
那根据这个扩容规则,电脑架构是x86,开头遇见的问题在执行method1方法后,bucket里面只有一个class,是因为在执行method的时候,进行了扩容,所以只有一个class方法。