这是我参与更文挑战的第8天,活动详情查看: 更文挑战
cache的数据结构
我们知道类在底层的结构如下图所示:
在之前的章节中我们研究了bits的结构分析,今天我们来研究cache_t cache的结构,我们在之前研究bits的时候使用LLDB调试我们偏移了0x20,根据计算如果我们研究cache那么我们需要偏移0x10:
通过查看objc源码中cache_t的数据结构,我们发现与lldb分析出的数据结构是一样的:
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;
};
}
__LP64__表示支持Unix和Unix类的系统(Linux,MacOS),所以此处的_flags是生效的
cache_t cache既然是缓存,那么无非缓存的就是属性、方法或者其他的一些东西,那么这个数据结构中那些数据是我们需要重点研究的呢?既然是缓存,那么根据一般常识来说,肯定会有相关的增删改查的操作,那么我们可以在cache_t的数据结构中向下寻找是否有增删改查的操作:
果不其然,我们发现了insert的插入操作,那么我们看一下insert具体操作了什么数据:
经过对insert操作的分析,我们大致可以确定insert是针对bucket_t这样的一个类型的数据进行插入操作,那么bucket_t是什么数据类型呢?
在结构体bucket_t中有两个很重要的成员变量_sel和_imp,那么极有可能这里就是缓存了方法与实现的地方;
需要注意的是根据注释我们可以知道,
_imp和_sel的前后顺序与架构有关,在armv7*、i386和x86_64三种架构中,把_sel放在_imp前边更好,而在arm64的架构下,把_imp放在了_sel前边更好,这有可能是新架构针对此处进行的优化处理
查找流程如下:
caceh底层LLDB分析
_sel和_imp的查找
既然知道了查找流程,那么我们借用lldb进行分析:
我们使用_bucketsAndMaybeMask和_maybeMask都无法获取想要的数据,同样经过测试_flags、_originalPreoptCache也都无法获取数据,那么我们就需要在源码中寻找,是否有相应的方法可以获取我们想要的数据:
buckets()方法刚好返回我们需要的数据结构bucket_t,那么我们使用lldb验证一下:
我们成功获取了bucket_t这样一个数据结构的数据,但是查看的时候去发现并没有任何数据。这是为什么呢?既然是缓存,那么是不是需要先插入(调用方法),然后我们才能获取到数据:
在lldb中调用了run方法之后,再次查看数据我们发现_maybeMask的Value和_occupied两个地方的值都改变了,我们继续获取bucket_t:
依然没有数据,这事什么原因呢?通过观察返现(bucket_t *) $12 = 0x0000000101207c20返回的是一个指针地址,那么会不会通过指针平移能够寻找到蛛丝马迹呢?
(lldb) p $9.buckets()[0]
(bucket_t) $15 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $9.buckets()[1]
(bucket_t) $16 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $9.buckets()[2]
(bucket_t) $17 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $9.buckets()[3]
(bucket_t) $18 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $9.buckets()[4]
(bucket_t) $19 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 45536
}
}
}
(lldb) p $9.buckets()[5]
(bucket_t) $20 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $9.buckets()[6]
(bucket_t) $21 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $9.buckets()[7]
(bucket_t) $22 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = (null)
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 4313873440
}
}
}
(lldb)
在lldb中通过指针平移进行调试,发现$9.buckets()[4]是有数据的,而$9.buckets()[7]的Value过大,很明显内存出错了,既然$9.buckets()[4]有值,那我先分析它的数据,刚才调用了方法run,那么我们就需要找到它的缓存数据,查看bucket_t源码发现:
进行验证:
虽然我们找到了想要的缓存数据,但是去难免有以下几个疑惑:
- 获取
sel可以直接获取,为什么imp却需要传入Class才能获取? - 为什么调用了
run方法之后,_maybeMask的Value变为了7,而_occupied的Value值为1? - 为什么指针平移到了
6号位置才找到了数据? - 为什么能够想到通过指针平移的方法来获取数据?
要解决这些疑惑,我们可以通过模拟源码的方式来分析
cache的流程
cache脱离源码编译分析
如何脱离源码分析呢?可以通过根据源码自定义数据结构的方式模拟源码调用流程:
自定义数据结构
我们定义一个Person类,如下:
然后在main文件中定义如下数据结构(模拟源码创建):
typedef uint32_t mask_t;
struct bucket_t_T {
SEL _sel;
IMP _imp;
};
struct class_data_bits_t_T {
uintptr_t bits;
};
struct cache_t_T {
bucket_t_T *_buckets;
mask_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
struct objc_class_T {
Class ISA; // 注意此处不能少
Class superclass;
cache_t_T cache;
class_data_bits_t_T bits;
};
void test(objc_class_T *cls) {
for (mask_t i = 0; i < cls->cache._occupied; i++) {
struct bucket_t_T bucket = cls->cache._buckets[i];
NSLog(@"-->%@-->%p", NSStringFromSelector(bucket._sel), bucket._imp);
}
}
接下来我们进行测试:
1.调用一个run方法:
2.再添加一个init方法:
结论1:父类的
init方法可以被缓存,_occupied的值好像是当前缓存的方法个数
3.再添加一个talk方法:
结论2:调用了3个方法之后
_occupied和_maybeMask的值从最开始的1、3变成了1、7,并且缓存的数据被清空
4.把talk方法缓存类方法haha:
结论3:类方法不会对当前类的缓存
cache造成影响;
疑问:
- 为什么调用了三个方法后
_occupied和_maybeMask的值从最开始的1、3变成了1、7? - 为什么数据被清空了?
cache底层原理分析
想要解决以上两个问题,我们需要找到一个切入点,很明显,这个切入点应该是插入操作
开辟空间
// 第一次进来occupied() 为0,newOccupied = 0 + 1 为1
mask_t newOccupied = occupied() + 1;
然后进入
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE; // 结果为 1 << 2 = 4
reallocate(oldCapacity, capacity, /* freeOld */false);
}
调用了
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
// 根据 newCapacity = 4 创建 bucket_t
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);
setBucketsAndMask(newBuckets, newCapacity - 1); // 4 - 1
if (freeOld) { // 为 false
collect_free(oldBuckets, oldCapacity);
}
}
调用
// 代码被简化
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
_maybeMask.store(newMask, memory_order_release); // 存储newMask = 3
_occupied = 0; // _occupied = 0
}
创建bucket_t
bucket_t *b = buckets();
mask_t m = capacity - 1; // m = 4 - 1
mask_t begin = cache_hash(sel, m); // sel 和 m 哈希算法得到搜索的开始地址
mask_t i = begin;
结果:_occupied为0,_maybeMask = 3
存储
mask_t newOccupied = occupied() + 1; // newOccupied = 0 + 1 为1
unsigned oldCapacity = capacity(), capacity = oldCapacity; // capacity = 3 ? 3+1 : 0 为4
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.
// 判断 1 + 1 <= 4 * 3 / 4
// 小于则进入不执行操作,也就是newOccupied < 3的时候
}
bucket_t *b = buckets();
mask_t m = capacity - 1; // m = 4 - 1
mask_t begin = cache_hash(sel, m); // sel 和 m 哈希算法得到搜索的开始地址
mask_t i = begin;
do {
if (fastpath(b[i].sel() == 0)) { // 第一次肯定不存在,进入判断条件
incrementOccupied(); // _occupied++ = 1
b[i].set<Atomic, Encoded>(b, sel, imp, cls()); // 存储 sel imp cls
return;
}
if (b[i].sel() == sel) {
return;
}
} while (fastpath((i = cache_next(i, m)) != begin)); // 循环哈希查找条件
结果:第一次存储_occupied为1,_maybeMask 为 3
第三次存储
mask_t newOccupied = occupied() + 1; // newOccupied = 2 + 1 为3
unsigned oldCapacity = capacity(), capacity = oldCapacity; // capacity = 3 ? 3+1 : 0 为4
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.
// 判断 3 + 1 <= 4 * 3 / 4
// 不满足条件,执行下一个else
} else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // capacity = 4 ? (4 * 2) : 4 为 8 扩容了
if (capacity > MAX_CACHE_SIZE) { // 最大扩容空间
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true); // 将_occupied置为 0
}
bucket_t *b = buckets();
mask_t m = capacity - 1; // m = 8 - 1
mask_t begin = cache_hash(sel, m); // sel 和 m 哈希算法得到搜索的开始地址
mask_t i = begin;
do {
if (fastpath(b[i].sel() == 0)) { // 缓存被清空,没有数据,进入判断条件
incrementOccupied(); // _occupied++ = 1
b[i].set<Atomic, Encoded>(b, sel, imp, cls()); // 存储 sel imp cls
return;
}
if (b[i].sel() == sel) {
return;
}
} while (fastpath((i = cache_next(i, m)) != begin)); // 循环查找合适的哈希下标查找操作
第三次方法的缓存扩容之后,执行了以下两个方法,缓存被清空
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
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);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
void cache_t::collect_free(bucket_t *data, mask_t capacity)
{
#if CONFIG_USE_CACHE_LOCK
cacheUpdateLock.assertLocked();
#else
runtimeLock.assertLocked();
#endif
if (PrintCaches) recordDeadCache(capacity);
_garbage_make_room ();
garbage_byte_size += cache_t::bytesForCapacity(capacity);
garbage_refs[garbage_count++] = data;
cache_t::collectNolock(false);
}
结论:第三次存储_occupied为1,_maybeMask 为 7
_occupied为当前缓存占用数量,_maybeMask为容量个数-1
lldb调用方法时关于_maybeMask的补充
我们使用lldb第一次调用方法时,_maybeMask的值直接为7,这和我们使用代码操作时的值3是不一样的,是因为使用lldb调用方法是,内部会默认调用respondsToSelector:和class方法
验证
在源码的insert方法中添加以下打印:
我们使用lldb调用方法看一下打印结果:
结论:
lldb调用run方法时,会隐性的调用respondsToSelector:和class方法,当run方法被调用时,已经达到扩容条件,所以时扩容之后再缓存了run方法;