这篇,我们来探索objc_class中的cache_t,字面意思很好理解,cache就是缓存,具体cache原理是什么呢?又缓存了什么东西?我们来一探究竟。
cache_t结构
我们看到了void insert(SEL sel, IMP imp, id receiver);
这个insert方法里面有sel,imp,receiver这些参数。看出来这事一个添加方法到cache_t的函数。我们进到里面看看具体做了什么。
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
// Never cache before +initialize is done
if (slowpath(!cls()->isInitialized())) {
return;
}
if (isConstantOptimizedCache()) {
_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
cls()->nameForLogging());
}
#if DEBUG_TASK_THREADS
return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif
ASSERT(sel != 0 && cls()->isInitialized());
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
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.
}
#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
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
这段代码里,我们发现b.set<Atomic, Encoded>(b, sel, imp, cls());
方法把sel,imp,cls等添加到了b=bucket_t的结构体里,还有buckets(),capacity,cache_hash(sel, m)他们又做了什么? 让我们来逐一分析他们。
验证bucket_t 用来存储sel, imp
但是当我们多调用一个method1时,奇怪的事情却发生了...
原本存在buckets里面的方法都没有了
cache扩容规则
arm64结构下,当目前缓存的大小+1小于等于桶子的大小的7/8的时候不扩容,当桶子的大小小于等于8,并且目前缓存的大小+1小于等于桶子的大小的时候也不扩容(桶子小于8的时候存满了才扩容)。
x86_64结构下,当目前缓存的大小+1,再+1小于等于桶子大小的3/4的时候不扩容。
真机验证
我们的对象有n个静态方法,我们依次运行看看cache情况。
疑问解答
在阅读源码的时候,可能会有一些疑惑,这里我来归纳一下
1、bucket数据为什么会有丢失的情况?
原因是在扩容时,是将原有的内存全部清除了,再重新申请了内存导致的。
2、_mask是什么?
_mask是指掩码数据,用于在哈希算法或者哈希冲突算法中计算哈希下标,其中mask 等于capacity - 1。
3、_occupied 是什么?
_occupied表示哈希表中 sel-imp 的占用大小 (即可以理解为分配的内存中已经存储了sel-imp的的个数)。
-
init会导致occupied变化 -
属性赋值,也会隐式调用,导致occupied变化 -
方法调用,导致occupied变化
4、打印的 cache_t 中的 ocupied 为什么是从 2 开始?
这里是因为LGPerson通过alloc创建的对象,并对其两个属性赋值的原因,属性赋值,会隐式调用set方法,set方法的调用也会导致occupied变化。