objc_class底层cache_t详解
cache_t 结构解析
在类的底层原理探索 中我们了解了objc_class中存储了isa,superClass,cache,bits,今天我们来看下cache的作用和底层实现。
cache结构
这个结构并不能看出来cache的作用,所以我们通过内存偏移打印一下cache的内容
image.png
我们打印的信息和其数据结构一致,但是缓存的内容我们还是不知道,我们只能从源码中继续往下看。
image.png原码中有一个insert方法,应该是用来存入数据的。
进入这个insert函数来看一下
image.png
我们注意到insert方法的参数有SEL,IMP,receiver,并且将这些参数放在了bucket中,我们来验证一下
获取bucket_t的内存内容
bucket_t看到了sel和imp,但是输出的内容又看不懂了,来看看bucket_t的源码声明:
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__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
// Compute the ptrauth signing modifier from &_imp, newSel, and cls.
uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
}
// Sign newImp, with &_imp, newSel, and cls as modifiers.
uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
return (uintptr_t)
ptrauth_auth_and_resign(newImp,
ptrauth_key_function_pointer, 0,
ptrauth_key_process_dependent_code,
modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
}
public:
static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
return (IMP)imp;
}
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
inline void scribbleIMP(uintptr_t value) {
_imp.store(value, memory_order_relaxed);
}
template <Atomicity, IMPEncoding>
void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};
我们看到 调用sel()函数可以返回SEL,调用imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)可以返回IMP,于是就可以试试在内存上获取到的bucket_t去调用这两个函数:
调用sel()和imp()函数 cache_t
cache_t扩容
那方法能存多少呢?存满了又是如何扩容呢?buckets是如何扩容的?为什么我没调用class和respondsToSelector方法它们就缓存到了buckets里面了?
看下cache_t的insert函数的实现代码
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; // 第一次insert的时候occupied()即_occupied会是0,newOccupied会是1
// capacity的值就是buckets的长度
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// 如果cache为空,则分配 arm64下长度为2 x86_64下长度为4的buckets,reallocate里无需释放老buckets
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
// 给容量附上初始值,x86_64为4,arm64为2
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
// 在arm64下,缓存的大小 <= buckets长度的7/8 不扩容
// 在x86_64下,缓存的大小 <= buckets长度的3/4 不扩容
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 // 只有arm64才需要走这个判断
// 在arm64下,buckets的长度 < = 8 时,不扩容
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 { // 除却上面的逻辑,就是扩容逻辑了
// 对当前容量的2倍扩容,并且如果扩容后容量大小 大于 一个最大阈值,则设置为这个最大值
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
// 创建新的扩容后的buckets,释放旧的bukets
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets(); // 获取buckets数组指针
mask_t m = capacity - 1; // m是buckets的长度-1
mask_t begin = cache_hash(sel, m);// 通过hash计算出要插入的方法在buckets上的起始位置(begin不会超过buckets的长度-1)
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)) { // 当前hash计算出来的buckets在i的位置它有没有值,如果没有值就去存方法
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) { // 当前hash计算出来的buckets在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)); // 如果计算出来的起始位置i存在hash冲突的话,就通过cache_next去改变i的值(增大i)
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
从代码上看,第一次空了会进行初始化capacity的长度INIT_CACHE_SIZE.
image.png
image.png
在arm64架构下开辟一个长度为2的桶子,在x86_64架构下开辟长度为4的桶子
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.
}
在arm64架构下如果缓存大于等于桶子长度的7/8,在x86_64架构下缓存大小大于等于桶子长度的3/4 则什么也不干。
在arm64架构下,当桶子的长度小于等于8的时候什么也不干
当缓存长度大于(系统设定的)默认最大值就等于默认最大值
其他情况下需要扩容,扩容的大小为2倍。
所以我们就明白了为什么之前调用了方法之后会什么没有找到,arm64下,初始值为2,当第1个方法缓存的时候,则要进行两倍扩容为4,并且需要清除旧桶。所以instanceMethod在刚进来扩容的时候就被清除掉了,也就找不到了。而前面我们说到class方法和responseToSelector方法是我们在调试的时候通过lldb调用产生的。