一,首先让我们看看类的结构
除了具体的类信息,还有一个重要的结构-->方法缓存cache,本文就是说明它的运行机制.
方法缓存的结构如下1> 方法缓存里有三个属性:
1.1 bucket_t
维护着一个散列表,里面的元素是以方法名"SEL"为key,方法的实现"IMP"为value的字典,
1.2 _mask
它代表的是散列表的长度-1,最初分配的值是4, 它的作用是 传入的方法名SEL & _mask得到的这个值,就是方法保存的位置,
1.3 _occupied
已经缓存的方法数量,它的数量<= 3/4 * _mask,否则_mask会扩容为原来的两倍,直到<= (1<<16)
下面来看看它里面的判断逻辑是怎么样的
主要代码在这个位置
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
mask_t newOccupied = occupied() + 1; 插入新的方法缓存,数量+1
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {如果还没有缓存过方法
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE; 这是一个容量为4的宏,最初分配的容量就是4
reallocate(oldCapacity, capacity, /* freeOld */false);创建-分配内存
}
else if (fastpath(newOccupied <= capacity / 4 * 3)) {
保证缓存表存的方法数小于等于 容量的3/4
}
else {超过容量的3/4,容量翻倍
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {不能超过最大值,最大值是1<<16
capacity = MAX_CACHE_SIZE;
}
超过容量后,将之前缓存的方法全部清空 "true"
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin; 通过 sel & mask 计算出sel该存放的位置 "i"
如果计算出来的值没有缓存方法,则直接插入,保存;
如果已经有方法插入了,在__arm64__架构 i--(其他架构i++),即,如果被占用,往上走一格,还被占用继续往上走,
因为规则限定了3/4,所以肯定能找到没有保存方法的位置.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(sel, imp, cls); 保存方法
return;
}
if (b[i].sel() == sel) {
这种情况是其他线程,已经把方法添加到这里了,那就直接退出循环
return;
}
} while (fastpath((i = cache_next(i, m)) != begin)); "i"不等于初始位置 i--,继续循环.
说明:
1> 方法的存储并不是按照数组一样从上到下存储,而是通过 sel & mask来存储的,所以难免会存在内存利用率低,但是加快了方法查找的速度,即:空间换时间.
2> 方法缓存是先于isa的方法查找,就是说,缓存中找不到,再到自己的方法列表中查找,找到之后也会缓存到cache_t
中,如果是父类的方法,也是会缓存到自己的表当中的.
3> arm64 之后增加了很多 &mask的操作,获取具体的类信息,也是通过 bits & mask 来获取,里面存储的信息更多了(文中提到的mask 不同的地方,mask的值是不同的).
4> 如果cache_next(i,m)
循环到0,还未找到,赋值i == mask
,继续循环,直到i == begin
,证明没有缓存这个方法,这是最差的情况,相当于遍历了一遍数组.