1. 前言
在上一篇文章中,我们了解了类的底层结构等(详见类的底层小结),其中类有一个成员变量cache,其结构为cache_t的一个结构体,那么这篇文章主要对这个cache_t进行一下简单的分析。
取名cache_t,顾名思义就是进行缓存,那么缓存的是什么呢?为什么要缓存呢?本篇文章解开这层面纱。
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:812157648,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
2. cache_t缓存原理
首先看一下cache_t的结构(已省略方法部分):
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
}
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;
using MethodCacheIMP = IMP;
在cache_t结构中,主要有三个属性_buckets _mask _occupied。
在bucket_t结构中有
imp:MethodCacheIMP类型,记录方法的指针。
key:由方法名name转换而成,作为缓存方法的关键字。
所以通过结构可知cache_t缓存的是bucket_t结构数据,bucket_t结构又是对方法的一层封装,所以cache_t缓存的就是方法。
既然缓存的是方法,那么是什么时候开始缓存的呢?
不卖关子,当然是实例对象在调用方法的时候进行缓存,说的更专业点就是在发送objc_msgSend的时候,先去缓存中查找,如果没有找到,那么进行缓存。
下面看一个极为重要的方法,在调用方法的时候,底层首先判断这个方法有没有被缓存过,如果有,直接返回方法的imp,如果没有则要先进行方法缓存。具体如下(具体解释请详看注释):
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// Never cache before +initialize is done
if (!cls->isInitialized()) return;
// 判断方法是否已经缓存了,如果缓存了,直接返回,如果没有,则进入下面的逻辑进行缓存。
if (cache_getImp(cls, sel)) return;
// 通过类获取类的cache变量
cache_t *cache = getCache(cls);
// 将方法名转成对应的key
cache_key_t key = getKey(sel);
// 通过cache获取已经缓存的数量occupied,并且+1,得到新的缓存数量。
mask_t newOccupied = cache->occupied() + 1;
// 通过cache获取缓存空间大小。
mask_t capacity = cache->capacity();
/**
缓存策略:
1. 当缓存空间未开辟时,调用`reallocate`开辟缓存空间,初始空间为4.
2. 当新计算出来的缓存数量未超过总缓存空间的3/4的时候,直接进行缓存。
3. 当新计算出来的缓存数量超过总缓存空间的3/4的时候,进行扩容。
*/
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
cache->expand();
}
// 经过上面的逻辑,走到这就开始进行缓存了。
// 找到对应缓存位置的bucket.
bucket_t *bucket = cache->find(key, receiver);
// 如果可以缓存的话,那么将occupied++。
if (bucket->key() == 0) cache->incrementOccupied();
// 给bucket进行赋值。
bucket->set(key, imp);
}
下面看一下开发缓存空间的方法reallocate:
// 该方法既在第一次开辟缓存空间的时候调用,也在扩容的时候调用。
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
/**
是否可以释放旧缓存空间的标志位。
如果当前缓存空间已经开辟了,那么该方法返回true,否则返回false。
同样也可以理解为如果是再扩容的时候,开辟新的缓存空间,释放旧的缓存空间的一个标志。
*/
bool freeOld = canBeFreed();
// 获取旧的缓存空间
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) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
下面再来看一下扩容的方法expand:
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
// 获取旧容量
uint32_t oldCapacity = capacity();
// 计算新容量,采用2倍扩容原理,如果oldCapacity为0,那么初始值设置为INIT_CACHE_SIZE,即4.
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
// mask overflow - can't grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}
// 开辟新空间,释放就空间。
reallocate(oldCapacity, newCapacity);
}
3. 结束语
cache_t的原理还是比较简单的,在最后做一个简单的总结:
实例对象在调用方法的时候,即会调用objc_msgSend方法,实际也就是去寻找方法的imp.
经过底层的一些方法调用后,最终会调用到cache_fill_nolock方法。
在cache_fill_nolock方法中,先会判断当前缓存空间里面是否缓存了该方法,如果缓存了,那么即得到了imp,如果没有缓存,那么继续执行缓存逻辑。
如果没有缓存方法,那么应该缓存这个方法,首先应判断缓存空间是否已经开辟,如果没有开辟,那么调用reallocate方法进行开辟,并设置初始值。
如果缓存方法的时候,缓存空间已经开辟了,那么继续判断算上当前这个方法的总缓存数量是否超过了总缓存空间容量的3/4,如果没超过,只 执行缓存方法,如果超过了,那么需要通过expand方法扩容。
扩容方法里面则会重新开辟一个大小为原空间2倍的新缓存空间,并且释放掉原缓存空间。
最后进行当前方法的缓存,找到对应缓存位置的bucket,设置bucket的各项值。
以上就是cache_t的缓存方法机制,如果有不对的地方,还请路过的朋友指正,如果觉得有用的话,别忘了给个赞啊!
原文作者:Daniel_Coder