前面我们分析了objc_class
结构体中的class_data_bits_t bits
,今天我们来研究一下cache_t cache
这个缓存。
// objc_class结构体
struct objc_class : objc_object {
// 类对象isa指针,占8字节
Class ISA;
// 指向父类的指针,占8字节
Class superclass;
// cache缓存,占16字节
cache_t cache;
// 类对象中存储的数据
class_data_bits_t bits;
...
...
}
cache源码分析
我们通过对类对象的内存首地址进行内存偏移16个字节查看一下cache_t cache
的内存结构
创建一个MyClass
类
@interface MyClass : NSObject
- (void)method1;
- (void)method2;
- (void)method3;
- (void)method4;
@end
@implementation MyClass
- (void)method1 {
NSLog(@"%s", __func__);
}
- (void)method2 {
NSLog(@"%s", __func__);
}
- (void)method3 {
NSLog(@"%s", __func__);
}
- (void)method4 {
NSLog(@"%s", __func__);
}
@end
断点调试打印一下MyClass类对象的内存地址
类对象的内存首地址为0x100008188
,偏移16个字节得出cache的内存首地址为0x100008198
= 0x100008188
+ 0x10
。获取一下cache里面存储的内容
获取到cache
里面的内容发现并不好观察,我们去源码里查找看一下cache_t
这个结构体
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
...
...
public:
struct bucket_t *buckets() const;
void insert(SEL sel, IMP imp, id receiver);
...
...
}
一、结构解析
从源码里我们可以看到cache_t
中我们刚才打印的值,存储的是_bucketsAndMaybeMask
和一个联合体
。在内存中占16个字节大小
- 1、其中
_bucketsAndMaybeMask
是uintptr_t
类型,也就是说_bucketsAndMaybeMask
是unsigned long
类型,内存大小占8个字节。
#ifndef __has_attribute
typedef unsigned long uintptr_t;
#else
typedef unsigned long uintptr_t;
#endif /* __has_attribute */
- 2、联合体中
_maybeMask
是mask_t
类型,占4个字节;_flags
占2个字节;_occupied
占2个字节;_originalPreoptCache
指针占8个字节。总体来说,联合体整体占8个字节。
typedef unsigned int uint32_t;
#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 unsigned short uint16_t;
同时还提供有两个方法buckets()
和void insert(SEL sel, IMP imp, id receiver);
二、cache的插入和扩容
从名字我们也可以看出void insert(SEL sel, IMP imp, id receiver);
方法是向cache
缓存中插入数据的,从参数里可以看出缓存插入的值是方法,也就是说cache是用来缓存方法
的。接着我们进入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;
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 存储方法的hash表
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
}
1、缓存写入
在void insert(SEL sel, IMP imp, id receiver);
方法内我们看到有个bucket_t *b = buckets();
,还有个do...while
循环,在循环内部b
变量调用了set
方法。我们先看一下b
变量存储的内容,进入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
...
...
public:
inline SEL sel() const {return _sel.load(memory_order_relaxed);}
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
}
...
...
};
从bucket_t
结构体内我们看到bucket_t
中存储的是sel和imp
,也就是说bucket_t
中存储的就是类的方法。同时还提供了两个方法:sel()
来获取方法名,imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)
来获取方法的函数实现。
知道bucket_t
中存储的是方法之后,再接着看do...while
循环中调用的set
方法中做了什么操作。
在set方法中其实就是在保存类的方法
,而所插入的下标i是通过cache_hash(sel, m)
来获取的
其中sel我们知道是方法名,而m = capacity - 1
。capacity
又是通过capacity()
方法获取的
unsigned oldCapacity = capacity(), capacity = oldCapacity;
在capacity()
方法内又调用了mask()
方法
unsigned cache_t::capacity() const
{
return mask() ? mask()+1 : 0;
}
在mask()
方法内只做了一个操作,就是获取_maybeMask
的值。而_maybeMask = bucket_t的长度 - 1
。
mask_t cache_t::mask() const
{
return _maybeMask.load(memory_order_relaxed);
}
也就是说capacity = bucket_t的长度
,m = bucket_t的长度 - 1
。m的值了解了,我们再看一下cache_hash(sel, m)
方法内的操作
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
其中uintptr_t value = (uintptr_t)sel;
对sel
转换得到的value
值是一个比较大的数字。对两个数进行&
得到的值不会超过较小的那个数。所以value & mask
得到的返回值最大就是mask
,也就是bucket_t的长度 - 1
。set
和插入下标的获取
我们知道了之后还有一个cache_next(i, m)
方法
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
cache_next
很简单就是查找下一个存储位置,那么至此do...while
循环中的逻辑就很清晰了
2、cache扩容
① cache扩容引出
既然我们知道了方法是缓存在bucket_t
中,那我们去类对象中找一下。前面我们分析得知cache_t
中提供的buckets()
方法可以获取到bucket_t
,调用一下buckets()
方法
得到bucket_t
的地址之后,我们再通过bucket_t
中提供的sel()
方法来获取缓存的方法名
我们在缓存中找到了两个方法respondsToSelector:
和class
,但是这两个方法在代码中并没有去调用。这是什么时候调用的呢?带着这个疑问我们在代码中调用一下p对象的方法method1
,然后再打印一下bucket_t
中缓存的方法
在bucket_t
中只找到了一个class
方法,method1
不仅没有找到,刚才打印的respondsToSelector:
方法也丢失了。这个现象出现的原因就是因为cache的扩容
在insert
方法中do...while
循环前我们可以看到这段代码的执行
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);
}
② 条件判断1
第一个条件判断if (slowpath(isConstantEmptyCache()))
判断当缓存为空时,即第一次向缓存中插入数据时会执行if (!capacity) capacity = INIT_CACHE_SIZE;
和reallocate(oldCapacity, capacity, false);
其中isConstantEmptyCache()
如下
bool cache_t::isConstantEmptyCache() const
{
return
occupied() == 0 &&
buckets() == emptyBucketsForCapacity(capacity(), false);
}
我们前面已经分析了capacity
= bucket_t的长度
,那么当buckets() = 0
时,capacity
会进行初始化赋值为INIT_CACHE_SIZE
。而INIT_CACHE_SIZE
相关计算如下
CACHE_END_MARKER
在__x86_64__
架构下值为1
,在__arm64__
架构下值为0
。那么INIT_CACHE_SIZE_LOG2
的值在__x86_64__
架构下为2
,在__arm64__
架构下为1
。所以在__x86_64__
架构下INIT_CACHE_SIZE
值为4 = (1 << 2)
,在__arm64__
架构下INIT_CACHE_SIZE
值为2 = (1 << 1)
。那么capacity
初始化时在__x86_64__
架构下capacity = 4
,在__arm64__
架构下capacity = 2
。也就是说当cache
缓存为空时,在__x86_64__架构下会开辟一个长度为4的桶子
,在__arm64__架构下会开辟一个长度为2的桶子
。初始化完了之后会调用reallocate(oldCapacity, capacity, false);
方法把capacity
和oldCapacity
传进去。reallocate
方法内部实现如下,当第一次缓存的时候,没有老的桶子,所以初始化时freeOld
传入的是false。
③ 条件判断2
第二个条件判断fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))
。其中newOccupied = occupied() + 1
,occupied()
如下
mask_t cache_t::occupied() const
{
return _occupied;
}
那么newOccupied = 缓存的大小
,而CACHE_END_MARKER
和cache_fill_ratio
在不同架构下的表述如下
其中cache_fill_ratio
在__x86_64__架构下为bucket_t长度的4分之3
,在__arm64__ && __LP64__架构下为bucket_t长度的8分之7
。也就是说在__arm64__ && __LP64__架构下缓存的大小 <= 桶子长度的7/8
和在__x86_64__架构下缓存的大小 < 桶子长度的3/4
,则什么也不干。
④ 条件判断3
第三个条件判断capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity
。其中CACHE_ALLOW_FULL_UTILIZATION
的定义在__arm64__ && __LP64__
的架构下
那就是在__arm64__ && __LP64__
架构下才会有可能执行这块代码。capacity
、newOccupied
和CACHE_END_MARKER
的定义我们前面已经分析了。其中FULL_UTILIZATION_CACHE_SIZE
的值计算如下
FULL_UTILIZATION_CACHE_SIZE值为8 = 1 << 3
,也就是说在__arm64__ && __LP64__
架构下当桶子的长度 <= 8 且 缓存的大小 <= 桶子的长度
。则什么也不干。允许小桶的缓存利用率为100%
⑤ 条件判断4
以上三种条件都不满足的情况下capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
。桶子进行2倍扩容
。且有个极限值判断
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
极限值MAX_CACHE_SIZE = 1 << 16
。接着会执行reallocate(oldCapacity, capacity, true);
此时freeOld
传入的是true
。也就是这一步会把老的桶子释放掉。
⑥ 案例
前面我们还遗留了一个问题respondsToSelector:
和class
方法到底是在什么时候调用的,既然我们cache必然会调用insert(SEL sel, IMP imp, id receiver)
方法插入数据,那么我们就在此方法内打印一下进入缓存的方法
断点调试在调用method1
方法前并没有发现respondsToSelector:
和class
方法的调用
此时再打印一下MyClass类对象的地址
此时发现respondsToSelector:
和class
被调用了,也就是说这两个方法是LLDB调试所调用的方法。同时我们前面所遇到的respondsToSelector:
和method1
方法丢失的问题应该也能想到是因为cache扩容导致的。
总结:cache扩容规则
- 1、cache桶子开辟的初始长度在arm64架构下为2,在x86_64架构下为4
- 2、在x86_64架构下:当缓存的大小等于桶子长度的3/4的时候进行2倍扩容
- 3、在arm64架构下:当缓存的大小大于桶子长度的7/8的时候,进行2倍扩容;当桶子长度小于等于8且未缓存满时,不会扩容