oc-底层原理分析之Cache_t
在类的结构分析一文中我们探索了类的底层定义,其中的属性Cache_t
我们并没有深入研究,这一篇文章我们来深入探索一下Cache_t
注意:以下的源码解读都是在mac电脑上运行,也就是说基于x86的结构,请记住这一点
什么是Cache_t
要搞清楚什么是Cache_t
和Cache_t
用来做什么,我们先看看在objc源码中,Cache_t的定义
struct cache_t {
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
uint16_t _occupied;
public:
static bucket_t *emptyBuckets();
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
//部分代码已略
}
通过源码我们看到Cache_t结构体中定义了三个属性:
_buckets
_mask
_occupied
但是我们现在并不知道这三个属性用来做什么,要搞清楚这三个属性的作用,我们通过一个例子来探索一下
先定义一个WPerson类:
@interface WPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end
@implementation WPerson
- (void)sayHello{
NSLog(@"WPerson say : %s",__func__);
}
- (void)sayCode{
NSLog(@"WPerson say : %s",__func__);
}
- (void)sayMaster{
NSLog(@"WPerson say : %s",__func__);
}
- (void)sayNB{
NSLog(@"WPerson say : %s",__func__);
}
+ (void)sayHappy{
NSLog(@"WPerson say : %s",__func__);
}
@end
现在我们创建一个WPerson
对象,然后调用sayHello
方法:
int main(int argc, const char * argv[]) {
@autoreleasepool {
WPerson *p = [WPerson alloc];
Class pClass = [WPerson class];
[p sayHello];
NSLog(@"%@",pClass);
}
return 0;
}
Cache_t 结构探索
先找到pClass
的首地址:
-
x/4gx pClass
:以16进制形式打印出pClass
地址0x100002288: 0x0000000100002260 0x0000000100334140 0x100002298: 0x00000001006f4050 0x0001802400000003
pClass首地址为:0x100002288
-
通过在类结构分析一文中我们得知了类的结构如下:
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() const { return bits.data(); } }
由于
isa
和superclass
都占用8个字节,所以我们要访问到cache
,我们需要将首地址偏移16字节
,所以:(lldb) p (cache_t *)0x100002298 (cache_t *) $1 = 0x0000000100002298
我们得到了
cache
的地址 -
访问cache.buckets(),我们知道
_buckets
是一个数组,所以我们先访问第一个值看存储的是什么(lldb) p $2.buckets()[0] (bucket_t) $3 = { _sel = { std::__1::atomic<objc_selector *> = "" } _imp = { std::__1::atomic<unsigned long> = 11912 }
}
```
我们得到一个bucket_t
结构,我们再看看bucket_t
的源码:
```c
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::memory_order_relaxed); }
inline IMP imp(Class cls) const {
uintptr_t imp = _imp.load(memory_order::memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order::memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(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`有两个属性`_sel`和`_imp`,看到这里是不是很熟悉,但是别急,我们先来打印一下sel的值
-
打印sel
(lldb) p $3.sel() (SEL) $4 = "sayHello"
我们看到结果打印出了我们刚刚调用的方法sayHello
,我们如果多调用几个方法,这里可以打印出多个方法
所以我们得出结论:
cache_t
用来缓存类的sel
以及imp
既然我们知道了cache_t用来缓存类的方法,那么还有一些疑问:
- 缓存的策略是什么呢?
- 如果空间不足,如何对空间进行扩容?
- 缓存又是怎么读取的?(这部分内容接下来会补上)
带着这三个疑问,我们开始探索
cache_t缓存策略
我们先来看看insert()
方法
ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
cacheUpdateLock.assertLocked();
#else
runtimeLock.assertLocked();
#endif
ASSERT(sel != 0 && cls->isInitialized());
// Use the cache as-is if it is less than 3/4 full
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 <= capacity / 4 * 3)) { // 4 3 + 1 bucket cache_t
// Cache is less than 3/4 full. Use it as-is.
}
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 扩容两倍 4
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true); // 内存 库容完毕
}
从这里我们可以看到:
- 如果
buckets
还未初始化,则会先调用reallocate()
方法对buckets
进行初始化,初始的存储大小为INIT_CACHE_SIZE
我们看到INIT_CACHE_SIZE
定义为(1 << INIT_CACHE_SIZE_LOG2)
也就是4 - 如果本次插入后所占用的空间小于总空间的
3/4
时,则直接进行数据插入 - 如果本次插入后所占用的空间
>=3/4
,则需要对总空间进行扩容,如何进行的扩容,在cache_t扩容
部分会有讲解
我们知道了在_buckets
中存储的是bucket_t
类型,当数据insert的时候,都会创建一个bucket_t
变量
mask
_buckets
是一个数组,如果我们要通过某个方法的sel
去查找imp
,我们怎么查找呢?我们大概率会想去去遍历_buckets
,但是这样的效率是低下的,每一次的方法查找都会遍历整个缓存,那么有没有什么办法能不遍历呢?
我们来看看源码中采用的方式,我们在源码中能看到这样一个方法:
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
mask传入的是mask_t m = capacity - 1;
也就是当前的容量 - 1
。通过和mask相与,我们得到的数字肯定是小于等于mask的,通过这种方式就可以得到sel和数组index的对应关系,在查找的时候就可以直接通过sel
得到数组对应的index
,不再需要遍历整个数组
但是你可能有一个疑问,这样不会出现编码的冲突吗?不同的sel
会不会得到同一个index呢?答案是会的,源码中也解决了这个问题
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(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));
如果index存在了,就会调用cache_next
重新生成一个index来存储,直到找到合适的位置
cache_t扩容
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 扩容两倍
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true); // 内存 库容完毕
我们可以看到扩容的原则是当前容量的两倍,并且扩容时,重新调用reallocate
将原来的数据清空。也就是说扩容后,原来的数据将不存在,重新调用原有方法的时候才会重新进行缓存,如果你这时候去打印cache
中的所有数据,得到的并不是你当前调用的所有方法,也能得到验证