前言
在前几篇文章对类的结构进行了初步的探索,研究了objc_class的内部结构,以及上一篇先跳过了cache_t
,而通过内存平移对class_data_bits_t
的结构进行了分析,了解了bits
存储在class_rw_t
内的methods
、properties
、protocols
等数据,这篇着重了解一下cache_t
的内部结构,也就是类缓存里存放的什么数据?以及如何进行缓存的?
cache_t的结构图
cache_t代码
在了解cache_t
的结构之前,还是把源码中的结构代码复制过来,来了解cache的内部构造,代码如下:
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8字节
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4字节
#if __LP64__
uint16_t _flags; // 2字节
#endif
uint16_t _occupied; // 2字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8字节
};
}
结构图
根据上面代码结构我们可以简单的画个结构图,如下:
cache_t
里储存了_buckets
、_mask
、_flags
、_occupied
等信息,_buckets
里就存放了sel
和imp
,接下来我们通过代码验证一下cache_t
的内存结构。
LLDB调试
由上面的cache_t
的代码结构,我们得出了一个粗略的结构图,现在我们通过lldb
一步步调试了看看获取到的数据。
首先通过p/x LGPerson.clas
拿到LGPerson
的首地址0x0000000100008818
,然后内存平移16字节,也就是加上0x10
得到0x0000000100008828
,打印输出cache_t
结构的$1
,通过*$1
输出cache_t
的数据,从结果来看,得到的数据和上面的代码定义的一致,继续获取_bucketsAndMaybeMask
、_maybeMask
和_originalPreoptCache
,看看输出什么?
通过打印输出发现获取不到对应的值,也没有看到相关的sel
和imp
,再次去查找源码发现cache_t
结构中有insert
方法,而在insert
方法中包含了对bucket
的创建,接着我们通过bucket
的类型bucket_t
找到下面的定义:
// 不同架构储存sel和imp的顺序不一致
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
}
接下来我们通过调用buckets()
方法,看看输出
可以看到输出了sel
和imp
,但值是nil,原因是我们没有调用方法,继续调用方法,然后lldb重新开始跑一遍(需要更新内存数据),再打印输出
通过输出可以看到_maybeMask
已经有值了,说明上面调用[p say1]
方法已经成功了,但我们输出的sel
和imp
还是为空,这是什么原因?
通过打印buckets()[1]
可以看到sel
和imp
是有值的,这里buckets
有个概念是,它是哈希函数
,buckets
里存的是多个bucket
,而哈希函数
并不是从0开始存储的,是根据函数定义存储的,所以我们在1号位置有输出,关于哈希函数
相关知识点可以另行查阅,本篇不做过多解释;下面通过调用sel()
可以看到方法的输出,调用imp()
报错是由于缺少参数。
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)
源码分析
结合上面的lldb调试,发现调用方法后需要重新获取类的内存,然后一步一步执行获取cache_t
数据的操作,不方便调试,而且无法在没有源码的项目中进行调试,鉴于这个原因,我们可以按照源码的设计自定义一个objc_class
和cache_t
。
自定义cache结构
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
// bucket_t
struct kc_bucket_t {
SEL _sel;
IMP _imp;
};
// cache_t
struct kc_cache_t {
struct kc_bucket_t *_bukets; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
// class_data_bits_t
struct kc_class_data_bits_t {
uintptr_t bits;
};
// cache class
struct kc_objc_class {
Class isa;
Class superclass;
struct kc_cache_t cache;
struct kc_class_data_bits_t bits;
};
接下来我们在main函数里执行
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
Class pClass = p.class;
[p say1];
struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);
for (mask_t i = 0; i<kc_class->cache._maybeMask; i++) {
struct kc_bucket_t bucket = kc_class->cache._bukets[i];
NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
}
return 0;
}
代码运行
先只调用一个方法[p say1]
看一下结果
输出了say1
方法,并且下面输出2个(null) - 0x0f
,说明buckets
开辟了3个容量,存了say1
方法,另外2个空着,_occupied = 1
,_maybeMask = 3
,接下来调用3个方法看一下结果
可以看到打印了3个方法,但输出的只有say3
,_maybeMask
由原来的3
增加到7
,为什么会出这样的结果?接下来还是去源码中看看具体逻辑。
insert源码
首先看一下cache_t
中insert()
方法的部分源码(由于比较长,过滤掉一些判断条件和断言)
// Historical fill ratio of 75% (since the new objc runtime was introduced).
// 设置3/4的容量值,当超过这个限定值就扩容
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
// 给buckets开辟内存
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
setBucketsAndMask(newBuckets, newCapacity - 1);
// 如果是扩容需要回收之前buckets的内存
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1; // 初始化 occupied() = _occupied + 1; 首次是 0+1
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// 如果是首次cache为空,会进入当前判断条件
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
// 给capacity赋值,INIT_CACHE_SIZE: 1 << 2 = 4 capacity首次等于4
if (!capacity) capacity = INIT_CACHE_SIZE;
// 给bucket按照capacity容量开辟内存
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.
// 当bucket存储容量+1 <= 75%空间不作处理,下次继续插入
}
#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 {
// 当bucket插入超过3/4时进行原来的2倍扩容
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
// 再次分配内存给bucket
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1; // 4 - 1 = 3
mask_t begin = cache_hash(sel, m); // 哈希函数(sel & m)
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
循环判断当前bucket的sel是否在cache里存在,如果没有做插入,如果有不做处理
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)); // cache_next():哈希函数,对比和begin是否相等,不一致继续下一次循环
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
总结
从源码insert()
方法我们可以看到类中的方法会去查找cache
里有没有对应的方法,如果有就直接从cache
里获取,如果没有就插入到缓存中,所有的sel
和imp
都是存储在cache_t
结构的buckets
里,给buckets
开辟内存空间时有个3/4
容量的界定值的概念,当buckets
超出这个值就会以2倍的capacity
扩容,但在setBucketsAndMask
是capacity - 1
的,这就是上面例子中,输出的1 - 3
和1 - 7
的原因,而在第二次调用say1、say2、say3
三个方法时为什么只输出say3
一个,其他都是(null) - 0x0f
,是由于在调用say3
时进行了扩容,回收了之前的buckets
内存空间。
以上就是通过一个示例再结合源码对cache_t
进行的分析探索过程。