分析cache_t 之前先看一下 objc_class,类在底层的结构(objc-runtime-new.h中)。
我们知道了isa是指向元类的,superclass 是父类相关,bits里面存的是类的属性和实例变量以及实例方法。剩下的cache 看名字是缓存,现在来看一下具体的
struct objc_class : objc_object {
// Class ISA; // 8
Class superclass; // 8
cache_t cache; // 16 不是8 // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
可以看到 cache 是一个cache_t结构体,在objc源码的objc-runtime-new.h中可以看到定义
struct cache_t {
struct bucket_t *_buckets; // 8
mask_t _mask; // uint32_t 4
mask_t _occupied; // 4
public:
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
mask_t capacity();
bool isConstantEmptyCache();
bool canBeFreed();
static size_t bytesForCapacity(uint32_t cap);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void expand();
void reallocate(mask_t oldCapacity, mask_t newCapacity);
struct bucket_t * find(cache_key_t key, id receiver);
static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
typedef uint32_t mask_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__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }
void set(cache_key_t newKey, IMP newImp);
};
- cache_t 的内部定义了三个成员,分别为mask_t类型的 _mask和_occupied,以及一个bucket_t的结构体指针
- 其中 mask_t 可以看出是一个无符号 Int 类型,在64位下为 uint32_t
- 而 bucket_t 则是存放着 imp 和 key
1、cache_t
开始通过源码可以知道猜测这个cache_t 是用来缓存方法的。下面进行验证
断点打在 sayHello 调用之后。按前面的猜想,LGPerson 的allloc 、 class 、sayHello 方法都调用了都有缓存。
同时呢上一篇分析类的结构的时候,我们知道类的 实例(对象)方法 在类的 bits 里面存储。类方法 在元类中存储,按这个思路的话那么缓存应该也是对应的,进行验证。
实例(对象)方法 缓存
(lldb) x/4gx pClass
0x1000012e0: 0x001d8001000012b9 0x0000000100b36140
0x1000012f0: 0x0000000101e23c20 0x0000000100000003
(lldb) p (cache_t *)0x1000012f0
(cache_t *) $1 = 0x00000001000012f0
(lldb) p *$1
(cache_t) $2 = {
_buckets = 0x0000000101e23c20
_mask = 3
_occupied = 1
}
(lldb) p $2._buckets
(bucket_t *) $3 = 0x0000000101e23c20
(lldb) p *$3
(bucket_t) $4 = {
_key = 4294971020
_imp = 0x0000000100000c60 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
}
(lldb)
对象方法 缓存
(lldb) x/4gx person
0x1018037d0: 0x001d8001000012ed 0x0000000000000000
0x1018037e0: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x001d8001000012ed & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x00000001000012e8
(lldb) x/4gx $1
0x1000012e8: 0x001d8001000012c1 0x0000000100afe140
0x1000012f8: 0x000000010183e4c0 0x0000000200000003
(lldb) p (cache_t *)0x1000012f8
(cache_t *) $2 = 0x00000001000012f8
(lldb) p * $2
(cache_t) $3 = {
_buckets = 0x000000010183e4c0
_mask = 3
_occupied = 2
}
(lldb) p $3._buckets
(bucket_t *) $4 = 0x000000010183e4c0
(lldb) p *$4
(bucket_t) $5 = {
_key = 4298994200
_imp = 0x00000001003cc3b0 (libobjc.A.dylib`::+[NSObject alloc]() at NSObject.mm:2294)
}
(lldb)
通过LLDB调试,可以发现确实和 实例方法和类方法的存储是一只的,缓存也是,实例方法缓存在类中,类方法缓存在元类中
由上的打印结果 sayHello 和 alloc 可知
2、cache的策略
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [[LGPerson alloc] init];//1
Class pClass = [LGPerson class];
// cache_t 为什么没有 - 第一次
[person sayHello];//2
[person sayCode];//3
[person sayNB]; //4
// 临界点 - 清理 过去 - 扩容 - cache_t 缓存
// cache_t mask
// 来一个方法就缓存 - 有一个特殊的处理
struct lg_objc_class *lg_pClass = (__bridge struct lg_objc_class *)(pClass);
for (mask_t i = 0; i<lg_pClass->cache._mask; i++) {
struct lg_bucket_t bucket = lg_pClass->cache._buckets[i];
NSLog(@"%lu - %p",bucket._key,bucket._imp);
}
NSLog(@"%@ - %p",person,pClass);
}
return 0;
}
- 将断点打在 [person sayCode];
- 打印出来的结果 mask 是3
- 将断点打在 [person sayNB]; 之后
- 打印出来的结果 mask 是7
那么问题来了:
我们一共调用了 init,sayHello,sayCode,sayNB 一共4个实例方法。mask 显示 3 后是 7. 打印一下 buckets 看看结果,发现只有 _buckets[2] 里面存了一个我们最后调用的 sayNB,那么其他方法都去哪了?
2019-12-25 00:57:52.143504+0800 LGTest[3662:48762] LGPerson say : -[LGPerson sayHello]
2019-12-25 00:57:52.144031+0800 LGTest[3662:48762] LGPerson say : -[LGPerson sayCode]
2019-12-25 00:57:52.144133+0800 LGTest[3662:48762] LGPerson say : -[LGPerson sayNB]
(lldb) x/4gx pClass
0x1000012e8: 0x001d8001000012c1 0x0000000100b36140
0x1000012f8: 0x0000000101029950 0x0000000100000007
(lldb) p (cache_t *)0x1000012f8
(cache_t *) $1 = 0x00000001000012f8
(lldb) p *$1
(cache_t) $2 = {
_buckets = 0x0000000101029950
_mask = 7
_occupied = 1
}
(lldb) p $2._buckets
(bucket_t *) $3 = 0x0000000101029950
(lldb) p *$3
(bucket_t) $4 = {
_key = 0
_imp = 0x0000000000000000
}
(lldb) p $2._buckets[0]
(bucket_t) $5 = {
_key = 0
_imp = 0x0000000000000000
}
(lldb) p $2._buckets[1]
(bucket_t) $6 = {
_key = 0
_imp = 0x0000000000000000
}
(lldb) p $2._buckets[2]
(bucket_t) $7 = {
_key = 4294971026
_imp = 0x0000000100000ce0 (LGTest`-[LGPerson sayNB] at LGPerson.m:25)
}
(lldb) p $2._buckets[3]
(bucket_t) $8 = {
_key = 0
_imp = 0x0000000000000000
}
(lldb) p $2._buckets[5]
(bucket_t) $9 = {
_key = 0
_imp = 0x0000000000000000
}
(lldb) p $2._buckets[6]
(bucket_t) $10 = {
_key = 0
_imp = 0x0000000000000000
}
(lldb) p $2._buckets[7]
(bucket_t) $11 = {
_key = 0
_imp = 0x0000000000000000
}
分析源码
1、cache_t
源码最上边
2、ask_t mask()
mask_t cache_t::mask()
{
return _mask;
}
3、mask_t capacity();
继续搜索mask(),发现在capacity方法中有mask的相应操作
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
4、cache_t::expand()
全局搜索(objc-cache.mm) capacity() ,看一下在哪个地方调用了这个方法。找到了方法 cache_t::expand() 。方法内部看到了capacity 的调用,意思是,如果 oldCapacity 获取的值为0,那么久用 INIT_CACHE_SIZE(1<<2 1左移两位十六进制得到的 实际为4)来初始化,如果存在,那么就用 oldCapacity的2倍 来作为 newCapacity
/* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
// mask overflow - can't grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
cache_fill_nolock
虽然我看看到了在 expand() 中进行了扩容,但是什么条件下才会去执行扩容还是不知道,这个时候就全局(objc-cache.mm)的去搜索 expand() 找到了方法 cache_fill_nolock
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// Never cache before +initialize is done
if (!cls->isInitialized()) return;
// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
cache->expand();
}
// 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.
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
}
从 cache_fill_nolock 源码可以看到,如果没有初始化clas 直接返回,如果找到了对象方法的缓存也直接返回。
下面才是重点 newOccupied = cache->occupied() + 1; 用临时变量计算出加上当前方法之后新的缓存需要的内存大小 然后进行判断 ,如果当前所需要的内存大小 > 当前cache_t 已经开辟的内存大小的 3/4 ,那就执行 cache->expand(); 进行扩容。
知道了先进行判断,然后进行缓存,那就顺着这两个方法去看源码看看初开辟了多大的内存?然后扩容后的方法缓存为什么只剩下最后调用的 sayNB 的问题。
- 这个最初开辟了对大的内存空间,这个在 expand() 中看到了 INIT_CACHE_SIZE(1<<2 1左移两位十六进制得到的 实际为4) 最初开辟的空间大小是4
- 既然开辟的最初大小是4,那么最开始打印出来 mask 为什么是3呢?扩容后应该是4*2=8,但是扩容后的mask 实际是7(打印看到的)。分析
- 在 expand() 中可以看到
-
cache_t 里面 void reallocate(mask_t oldCapacity, mask_t newCapacity);
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) { bool freeOld = canBeFreed(); bucket_t *oldBuckets = 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); 这个地方是计算 mask 的大小,是用 newCapacity-1.类刚初始化的时候是4-1=3.第一次扩容是4*2-1=7 -1 是一种算法。这个地方之所以用-1.原因 1、 用3是因为3比4计算更精准,后边的计算超过 3/4 的时候能更精准的计算并提前扩容保证安全。 setBucketsAndMask(newBuckets, newCapacity - 1); if (freeOld) { cache_collect_free(oldBuckets, oldCapacity); cache_collect(false); } }
将计算出出来的 newMask 赋值给 mask
-
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask) { // ensure other threads see buckets contents before buckets pointer mega_barrier(); _buckets = newBuckets; // ensure other threads see new buckets before new mask mega_barrier(); _mask = newMask; _occupied = 0; }
扩容后把之前缓存器中的值全部删除掉
static void cache_collect_free(bucket_t *data, mask_t capacity) { cacheUpdateLock.assertLocked(); if (PrintCaches) recordDeadCache(capacity); _garbage_make_room (); garbage_byte_size += cache_t::bytesForCapacity(capacity); garbage_refs[garbage_count++] = data; }
- 分析源码之后知道 cache_fill_nolock 方法内部,可以发现,expand 之后,才会把最新调用的方法的 imp 和 key 缓存了下来,
- 如果不需要扩容直接存储。
- 如果需要扩容是重新开辟内存空间,然后把之前缓存器中的值全部删除。 这就是为什么 cache 扩容后我们打印出来的只有最后调用的 sayNB 方法,这里就适用了LRU算法,把最近调用过的方法缓存下来
expand 方法的最后,reallocate(oldCapacity, newCapacity)方法,在reallocate方法中,首先使用newCapacity初始化了一个newBuckets,之后设置了新的 buckets 以及 mask,并且最后释放了旧的 oldBuckets,这里之所以直接用 newBuckets 代替,而不是用追加或者修改 oldBuckets 的方式,主要还是为了安全以及执行效率
cache_fill_nolock 的最后 这个端代码就是 将方法缓存下来的操作。
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
总结:
- cache_fill_nolock 缓存入口进入
- 先看有没有缓存,又返回,没有继续找2
- getKey(sel)将方法名变成一个数字,便于查找
- 创建一个临时变量 occupied +1 (用于判断缓存是否超出开辟的内存大小)
- 开辟内存大小为4
- 返回的是4-1 3 mask
- capacity 是1
- 然后去存 set(key , imp)
- 有调用方法的时候,然后把缓存数量进行 +1
- 判断存储超过3/4的时候进行扩容
- 扩成原来的2倍 4*2 = 8
- 8-1 mask (这是个算法返回不让达到100%,也是出于安全考虑)
- 清除原来的缓存,存储当前的这个方法(copy不安全、同时为了速度更快所以要消除)
cache_fill_nolock 缓存入口进入
判断是否有缓存 return
cache_key_t key=getKey(sel)
读取开辟占用occupied
然后判断是否是空的缓存表 cache->isConstantEmptyCache()
- 是 :
cache->reallocate 创建一个new Buckets
_mask = newMask
_occupied = 0;
申请完毕后开始填充 bucket_t *bucket =cache->find(key,receiver); 这个就是上一步的reallocate 的时候创建的
然后给bucket 设值 set(key,imp)
- 否:
不是第一次缓存,已经存在缓存表
if(newOccupied<=capacity/4*3) 创建临时变量加上新缓存的方法是是否超过开辟内存的3/4
- 不超过
正常添加bucket
- 超过
cache->expand() 进行扩容 oldCapacity*2 扩容到原来的两倍
然后进行reallocate(oldCapacity,newCapacity); 清理旧内存,从新开辟创建,然后缓存方法,之前缓存的就被清理掉了。Reallocate同上
然后进行填充
bucket_t *bucket=cache->find(key,receiver);
if(bucket->key()==0)
cache->incrementOccupied();
bucket->set(key,imp);