我们在OC底层探索之对象原理(下)探索了isa
指针指向,在OC底层探索之类的探索(上)OC底层探索之类的探索(下))探索了ro
,rw
。cache
顾名思义是缓存,它到底缓存了什么怎么缓存的你?今天我们来探索cache
。
初探cache_t结构体
我们先看下cache_t
结构体结构,首先是一个_bucketsAndMaybeMask
它是uintptr_t
类型的占8字节内存;然后是一个联合体,我们知道联合是互质的,它是按照联合体内部最大成员变量来计算内存,_originalPreoptCache
是占8字节内存的,_maybeMask
是占4字节内存,_flags
是占2字节内存的,_occupied
是占2字节内存的,所以联合体内部的结构体占8字节内存,联合体占8字节内存,整个cache_t结构体是占16字节内存。所以根据内存平移规则我们获取bits
数据时需要平移isa
(8字节)+superclass
(8字节)+cache
(16字节)=32个字节的内存。
当然我们看cache_t
结构体内部数据看不出来什么。使用lldb
打印来看cache_t
到底存了什么东西。
我们使用objc-838
进行探索,新建一个LGPersion
,里面包含method1
、method2
、method3
、method4
等实例方法,method5
、method6
等类方法。
@interface LGPerson : NSObject
- (void)method1;
- (void)method2;
- (void)method3;
- (void)method4;
+ (void)method5;
+ (void)method6;
@end
打印cache
内容,我们也没发现有啥有效信息,很懵逼。那我们在看看cache_t
结构体:
我们发现cache
结构体里面有个buckets
函数返回一个bucket_t
的结构体。
打印
buckets
,打印bucket_t
结构体内容,我们也没发现有啥有效信息,很懵逼。那我们在看看
bucket_t
结构体,我们发现有_sel
和_imp
这2个参数和sel()
方法。尝试着打印_sel
看看。打印出来我们发现只有class
和respondsToSelector:
这2个方法,很是懵逼,没有我们的method1
和method2
,这是为什么呢?难道我们的method1
和method2
没有被缓存吗?带着这个疑问我们再回来看看cache_t
这个结构体。
cache扩容分析
我们初探cache_t结构体时没有发现我们的的method1
和method2
方法,那cache是到底是怎么缓存方法的呢?我们在cache_t结构体内部发现了insert方法(顾名思义插入方法)里面回传3个参数sel(方法名)、imp(方法实现)、receiver(接收者)。
点击查看insert方法。我们发现insert方法内部有个set方法,点击set方法内部我们发现一个store方法,store就是存储的意思,终于找到具体方法存储方法了。
那么就开始分析cache到底是怎么个操作流程。我们直接定位到insert方法有效代码段。
insert方法有效代码段:
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 *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);
首先我们先看下occupied()是这啥意思,可以看到occupied()内部实现,它直接返回一个_occupied,而_occupied是cache_t里面结构体的一个成员变量,初始化的时候它是0。所以newOccupied=occupied()+1 = 0+1 =1。
我们可以看到capacity = oldCapacity而oldCapacity = capacity(),看看capacity()内部实现,它是一个三目运算,点击进去mask()函数,我们可以看到其实它就是加载cache_t内部_maybeMask的数量,当然当第一次的是0,之后都是数量+1,其实也就是buckets的长度-1,那newOccupied的长度其实就是buckets的长度。
unsigned oldCapacity = capacity(), capacity = oldCapacity;
当我们第一次进来的时候cache是空的,
capacity = INIT_CACHE_SIZE
,我们可以看到INIT_CACHE_SIZE
在里面有个CACHE_END_MARKER
,CACHE_END_MARKER
定义在x86_64
下是1在arm64
是0,所以说INIT_CACHE_SIZE
在x86_64
下是1<<2 = 4,在arm64
下是1<<1 =2。所以capacity
在x86_64
下是4,在arm64
下是2。然后我们看看reallocate
函数具体实现,setBucketsAndMask
它是设置cache_t
结构体内部成员变量_bucketsAndMaybeMask
,freeOld
如果是true
它会释放oldBuckets
,是false
啥也不做。第一次来的时候freeOld
为false
所以不会被释放。
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))) {
我们之前分析newOccupied
是1,CACHE_END_MARKER
在x86_64
下是1在arm64
是0。capacity
是bucket
的长度,cache_fill_ratio函数则是在x86_64
下是bucket
长度的4分之3,在arm64
是bucket长度的8分之7。newOccupied + CACHE_END_MARKER
实际就是缓存的大小。
总结一下:缓存大小在arm64
下小于或等于bucket
(桶子)的8分之7或者在x86_64
下小于或等于bucket
(桶子)的4分之3,啥也不做。需要注意的是CACHE_END_MARKER
在x86_64
下是1在arm64
是0。当桶子的个数是4个的时候,第3个方法进来的时候,在x86_64
下newOccupied + CACHE_END_MARKER
是4,不满足条件,就需要扩容了,在x86_64
下newOccupied + CACHE_END_MARKER
是3,满足条件,是不需要扩容了。
FULL_UTILIZATION_CACHE_SIZE
的长度等于1<<3 = 8
,所以在arm64
架构下当桶子的长度小于8的时候啥也不做。
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
最后一个判断,当桶子的大小为0的时候,会给一个初始值
INIT_CACHE_SIZE
(我们上文提到INIT_CACHE_SIZE
在x86_64
下是4,在arm64
下是2),如果桶子大小不为0会进行2倍扩容。当桶子大小大于MAX_CACHE_SIZE
(1<<16为2的16次方),桶子大小为MAX_CACHE_SIZE
。我们上文分析reallocate
函数第三个参数freeOld
传true
的时候,老桶子会被释放。所以说我们在初探cache_t
结构体的method1
和method2
没被发现的原因,可能是cache
扩容了,method1
和method2
被释放了。
else {
capacity = capacity capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
总结:
cache扩容规则
- x86_64
1.当缓存的大小等于的4分之3时候,会进行2倍扩容。 - arm64
1.当缓存的大小大于桶子长度8分之7的时候,进行2倍扩容
2.当桶子的长度小于8的时候,不会扩容。
cache扩容验证
我们采用Intel
芯片的模拟器,是x86_64
环境的。所以说初始化桶子的大小是4。
我们通过之前分析,当前需要缓存数量(缓存的大小+1)到3个、8个、14个等等的时候,是需要扩容,好了,验证一下。
p objc_getClass("LGPerson")
:使用这个是为了防止之前p p.class
的时候会生成class
和respondsToSelector:
这2个方法。
当调用2个方法的时候,可以打印method1
、method2
,所以说没有进行扩容。
当调用2个方法的时候,可以打印method1
、method2
,所以说没有进行扩容,现在桶子大小是4。
当调用3个方法的时候,可以打印
method3
,没有打印method1
、method2
,所以进行扩容了,现在桶子大小是8。
(lldb) p objc_getClass("LGPerson")
(Class) method8
= 0x0000000100008288
(lldb) x/4gx 0x0000000100008288
0x100008288: 0x0000000100008260 0x0000000100800140
0x100008298: 0x0000000100b5f510 0x0001801000000007
(lldb) p (cache_t *)0x100008298
(cache_t *) = 0x0000000100008298
(lldb) p ->buckets()
(bucket_t *) = 0x0000000100b5f510
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +1
(bucket_t *) = 0x0000000100b5f520
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +2
(bucket_t *) = 0x0000000100b5f530
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +3
(bucket_t *) = 0x0000000100b5f540
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +4
(bucket_t *) = 0x0000000100b5f550
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +5
(bucket_t *) = 0x0000000100b5f560
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +6
(bucket_t *) = 0x0000000100b5f570
(lldb) p ->sel()
(SEL) = "method3"
(lldb) p +7
(bucket_t *) = 0x0000000100b5f580
(lldb) p ->sel()
(SEL) = ""
(lldb) p +8
(bucket_t *) = 0x0000000100b5f590
(lldb) p ->sel()
(SEL) = (null)
当调用8个方法的时候,可以打印method1
,没有打印method2
、method3
、method4
、method5
、method6
、method7
、lldb调试结果:
,所以进行扩容了,原来桶子大小是8,原桶子的数据被释放,扩容后的大小是16。
(lldb) p objc_getClass("LGPerson")
(Class) = 0x00000001000082b0
(lldb) x/4gx 0x00000001000082b0
0x1000082b0: 0x0000000100008288 0x0000000100800140
0x1000082c0: 0x0000000100c40d50 0x000180100000000f
(lldb) p (cache_t *)0x1000082c0
(cache_t *) = 0x00000001000082c0
(lldb) p ->buckets()
(bucket_t *) = 0x0000000100c40d50
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +1
(bucket_t *) = 0x0000000100c40d60
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +2
(bucket_t *) = 0x0000000100c40d70
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +3
(bucket_t *) = 0x0000000100c40d80
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +4
(bucket_t *) = 0x0000000100c40d90
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +5
(bucket_t *) = 0x0000000100c40da0
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +6
(bucket_t *) = 0x0000000100c40db0
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +7
(bucket_t *) = 0x0000000100c40dc0
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +8
(bucket_t *) = 0x0000000100c40dd0
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +9
(bucket_t *) = 0x0000000100c40de0
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +10
(bucket_t *) = 0x0000000100c40df0
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +11
(bucket_t *) = 0x0000000100c40e00
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +12
(bucket_t *) = 0x0000000100c40e10
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +13
(bucket_t *) = 0x0000000100c40e20
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +14
(bucket_t *) = 0x0000000100c40e30
(lldb) p ->sel()
(SEL) = "method8"
(lldb) p +15
(bucket_t *) = 0x0000000100c40e40
(lldb) p ->sel()
(SEL) = ""
(lldb) p +16
(bucket_t *) = 0x0000000100c40e50
(lldb) p ->sel()
(SEL) = (null)
(lldb) p +17
(bucket_t *) = 0x0000000100c40e60
(lldb) p ->sel()
(SEL) = ""
(lldb) p +18
(bucket_t *) = 0x0000000100c40e70
(lldb) p ->sel()
(SEL) = (null)