之前我们已经探究了bits的结构,本文将对类的一个重要成员--cache,从源码objc4-7.8.1层面进行分析。
cache_t成员探究
为了方便研究,我们可以先将cache_t的静态变量和方法屏蔽,简化为以下结构:
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic _buckets;
explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
可以看到,cache_t主要有4个成员--buckets、mask、flags、occupied。其中模拟器将buckets和mask分开,而在真机中则将mask和buckets合并为一个变量_maskAndBuckets。
那么,这些成员分别保存了什么信息呢?接下来一一探讨。
模拟源码分析
explicit_atomic
实际上只是起到一个线程安全的作用,_buckets实际上是bucket_t *
类型。
查看bucket_t的结构,发现它有_imp和_sel变量,我们可以猜测,它是存放缓存方法信息的变量。
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 _imp;
explicit_atomic _sel;
#else
explicit_atomic _sel;
explicit_atomic _imp;
#endif
...
};
我们可以通过一段代码进行验证。
同样是创建Animal类,添加4个方法
@interface Animal : NSObject
- (void)func1;
- (void)func2;
- (void)func3;
- (void)func4;
@end
@implementation Animal
- (void)func1 {
NSLog(@"%s", __func__);
}
- (void)func2 {
NSLog(@"%s", __func__);
}
- (void)func3 {
NSLog(@"%s", __func__);
}
- (void)func4 {
NSLog(@"%s", __func__);
}
@end
然后我们模拟一个objc_class类,将Animal的类转化为相同我们类结构,相当于一个镜像,从而可以在非源码环境获取类结构体中的信息。
代码如下:
typedef uint32_t mask_t;
struct lj_bucket_t {
SEL _sel;
IMP _imp;
};
struct lj_cache_t {
struct lj_bucket_t * _buckets;
mask_t _mask;
uint16_t _flags;
uint16_t _occupied;
};
struct lj_class_data_bits_t {
uintptr_t bits;
};
struct lj_objc_class {
Class ISA;
Class superclass;
struct lj_cache_t cache;
struct lj_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
Animal *obj = [Animal alloc];
Class pClass = [Animal class];
struct lj_objc_class *lj_pClass = (__bridge struct lj_objc_class *)(pClass);
NSLog(@"before occupied: %hu, mask: %u", lj_pClass->cache._occupied, lj_pClass->cache._mask);
[obj func1];
[obj func2];
NSLog(@"after occupied: %hu, mask: %u", lj_pClass->cache._occupied, lj_pClass->cache._mask);
for (mask_t i = 0; icache._mask; i++) {
struct lj_bucket_t bucket = lj_pClass->cache._buckets[i];
NSLog(@"%@ - %p", NSStringFromSelector(bucket._sel), bucket._imp);
}
}
return 0;
}
执行结果为:
2020-09-17 18:24:22.145891+0800 cache_t分析[28651:4825405] before occupied: 0, mask: 0
2020-09-17 18:24:22.145979+0800 cache_t分析[28651:4825405] -[Animal func1]
2020-09-17 18:24:22.146181+0800 cache_t分析[28651:4825405] -[Animal func2]
2020-09-17 18:24:22.146241+0800 cache_t分析[28651:4825405] after occupied: 2, mask: 3
2020-09-17 18:24:22.146827+0800 cache_t分析[28651:4825405] func2 - 0x2ca8
2020-09-17 18:24:22.146903+0800 cache_t分析[28651:4825405] (null) - 0x0
2020-09-17 18:24:22.147001+0800 cache_t分析[28651:4825405] func1 - 0x2cd8
我们可以看到,fun1和fun2执行之前,mask和occupied都为0,而在执行fun1和fun2之后,mask为3,occupied为2,同时在buckets中存储了fun1和fun2,并且不是顺序排列。
目前我们可以确定的是,方法缓存保存在buckets中。但是此时我们有了两个新问题:
- mask和occupied分别表示什么
- 为什么buckets的存放时乱序的
回归源码探究
重新回到objc4-7.8.1
源码,我们可以看到在objc-runtime-new.h
第317行有一个occupied的自增方法incrementOccupied()
,我们可以猜测答案与这个方法有关。全局搜索incrementOccupied
,可以发现objc-cache.mm中的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)) {
// Cache is less than 3/4 full. Use it as-is.
}
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 because the
// minimum size is 4 and we resized at 3/4 full.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set(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));
cache_t::bad_cache(receiver, (SEL)sel, cls);
}
这部分代码较多,分析之后可以大体分为2部分
1.开辟空间
开辟空间有3个分支,我们一一来看。
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)) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
1.1 isConstantEmptyCache()--当前缓存是否为空
当前对象如果没有调用过任何方法,那么缓存应该为空,如果此时需要插入缓存,肯定是需要开辟一块内存.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
// 获取旧的buckets
bucket_t *oldBuckets = buckets();
// 创建新的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);
// 创建buckets和mask
setBucketsAndMask(newBuckets, newCapacity - 1);
// 清除旧的buckets
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
}
}
通过代码可以看出,如果当前没有buckets,那么会通过cache_t::reallocate
开辟一个大小为INIT_CACHE_SIZE
内存(INIT_CACHE_SIZE为4),并且对mask复赋值,mask大小为当前可缓存方法数量-1。
1.2 newOccupied + CACHE_END_MARKER <= capacity / 4 * 3
查看newOccupied
的定义:
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
可以看到,如果执行完方法后occupied小于原有大小的3/4,则继续下面流程。
1.3 新增后occupied大于原有3/4
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
如果新增后occupied大于原有大小的3/4,那么原有的buckets空间显然已经不适合存储现有方法,他会在原有大小的基础乘以2重新开辟一块空间,并将原有空间清空。同时,重新对mask赋值。
2 将方法缓存到buckets中
这个过程先通过计算得出当前方法的哈希值,然后通过哈希值查看对应位置的内存是否已被占用,如果没有,则缓存该方法,如果有并且当前位置缓存的方法不相同,则再次哈希找下一个位置保存方法。
从这个过程我们也可以得知,buckets实际是一张哈希表。
bucket_t *b = buckets();
mask_t m = capacity - 1;
// 计算当前方法哈希值
// 这里采用的哈希算法是 sel & mask
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 because the
// minimum size is 4 and we resized at 3/4 full.
do {
if (fastpath(b[i].sel() == 0)) { // 没有发生哈希冲突,开始存储
incrementOccupied();
b[i].set(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)); // 当前方法发生哈希冲突,在哈希找位置
这也解释了为什么在一开始的示例中方法缓存并不是连续的。
总结
通过以上的探究,我们可以分析一下代码的执行结果
Animal *obj = [Animal alloc];
Class pClass = [Animal class];
struct lj_objc_class *lj_pClass = (__bridge struct lj_objc_class *)(pClass);
[obj func1];
[obj func2];
[obj func3];
[obj func4];
NSLog(@"occupied: %hu, mask: %u", lj_pClass->cache._occupied, lj_pClass->cache._mask);
for (mask_t i = 0; icache._mask; i++) {
struct lj_bucket_t bucket = lj_pClass->cache._buckets[i];
NSLog(@"%@ - %p", NSStringFromSelector(bucket._sel), bucket._imp);
}
当obj执行func3,缓存的方法数已经来到了3个,而此时的occupied已经是原有的3/4,所以需要缓存大小扩容到原来的2倍即8个bucket_t的大小,此时的mask为8-1=7,因为重新开辟空间所以缓存内原有的func1和func2被清空,最终缓存内只留下func3和func4,并且位置不定。
2020-09-18 14:27:25.269903+0800 cache_t分析[52626:5259235] -[Animal func1]
2020-09-18 14:27:25.269967+0800 cache_t分析[52626:5259235] -[Animal func2]
2020-09-18 14:27:25.270015+0800 cache_t分析[52626:5259235] -[Animal func3]
2020-09-18 14:27:25.270483+0800 cache_t分析[52626:5259235] -[Animal func4]
2020-09-18 14:27:25.270537+0800 cache_t分析[52626:5259235] occupied: 2, mask: 7
2020-09-18 14:27:25.270595+0800 cache_t分析[52626:5259235] (null) - 0x0
2020-09-18 14:27:25.270643+0800 cache_t分析[52626:5259235] (null) - 0x0
2020-09-18 14:27:25.270688+0800 cache_t分析[52626:5259235] (null) - 0x0
2020-09-18 14:27:25.270814+0800 cache_t分析[52626:5259235] func4 - 0x2fe8
2020-09-18 14:27:25.270867+0800 cache_t分析[52626:5259235] (null) - 0x0
2020-09-18 14:27:25.270945+0800 cache_t分析[52626:5259235] func3 - 0x2fb8
2020-09-18 14:27:25.270994+0800 cache_t分析[52626:5259235] (null) - 0x0
我们可以将cache的流程用下图描述: