OC 类原理探索 系列文章
- OC 类原理探索:类的结构分析
- OC 类原理探索:类结构分析补充
- OC 类原理探索:属性的底层原理
- OC 类原理探索:cache_t 的结构分析
前言
先回顾下类的结构:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
}
isa、superclass、bits在前几篇文章中都有聊过,cache是方法的缓存,今天来对cache的结构进行探索。
准备工作
- objc4-818.2 源码;
一、cache 的基本数据结构
打开 objc4-818.2 源码,查看cache_t的数据结构:
struct cache_t {
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
};
}
关于__LP64__我们可以看下面这张表:
根据表格可以看出,__LP64__在Mac OS X中是有效的,__LP64__等于1,所以cache结构中有_flags这个成员变量。
lldb 探索 cache 结构
lldb通过地址偏移找到cache:
cache我们虽然找到了,但是并没有找到类似list的这种和方法列表有关的变量,所以我们返回源码去寻找和增、删、改、查、列表有关的方法。
源码找方法
我们看到了emptyBuckets()清空方法和bucket_t的相关操作,点击查看
bucket_t。
最终找到了存储方法的数据结构bucket_t,bucket_t中有_sel和_imp两个成员变量。
SEL 和 IMP 的关系
SEL是方法编号,相当于书本目录的名称;IMP是函数指针地址,相当于书本目录的页码;函数实现相当于书本中具体的内容;- 查找过程
- 首先明白我们要找到书本的什么内容:
SEL目录里面的名称; - 然后通过目录名称找到对应的页码:
IMP; - 通过页码去定位具体的内容。
- 首先明白我们要找到书本的什么内容:
cache 的大体结构
探索到这里,可以得出cache的大体结构如下:
二、cache 底层 LLDB 分析
我们继续上面的lldb探索,在cache_t中找到buckets()函数:
struct cache_t {
...
struct bucket_t *buckets() const;
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);
}
...
}
在bucket_t中可以找到sel()函数和imp()函数:
struct bucket_t {
...
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
...
}
通过lldb打印cache中缓存的方法:
- 通过上面
lldb调试,打印buckets()[2]得到了缓存方法say1; - 但是
buckets()[0]、buckets()[1]是空值; _maybeMask的值是3,_occupied的值是1。
为什么会有这样的结果呢,我们接下来继续调试。
三、脱离源码 分析
- 我们之前一直都是用
lldb的方式来探索底层,接下来换一种方式来分析; - 新的方式是:创建和底层源码一样的数据结构,来进行底层的探索分析。
仿照底层源码,创建数据结构
typedef uint32_t mask_t;
struct ssl_bucket_t {
SEL _sel;
IMP _imp;
};
struct ssl_cache_t {
struct ssl_bucket_t *_buckets; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
struct ssl_class_data_bits_t {
uintptr_t bits;
};
struct ssl_objc_class {
Class isa;
Class superclass;
struct ssl_cache_t cache;
struct ssl_class_data_bits_t bits;
};
脱离源码 打印分析
添加下面的代码,用来打印_maybeMask、_occupied和方法缓存列表:
int main(int argc, const char * argv[]) {
@autoreleasepool {
SSLPerson *p = [SSLPerson alloc];
Class pClass = p.class;
struct ssl_objc_class *ssl_class = (__bridge struct ssl_objc_class *)(pClass);
NSLog(@"%hu - %u",ssl_class->cache._occupied,ssl_class->cache._maybeMask);
for (mask_t i = 0; i < ssl_class->cache._maybeMask; i++) {
struct ssl_bucket_t bucket = ssl_class->cache._buckets[i];
NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
}
}
return 0;
}
下面打印测试几种情况:
1. 调用say1方法
- 打印结果
_maybeMask的值是3,_occupied的值是1。- 成功打印了
say1方法。
- 为什么会有这样的结果呢,后面进行分析。
2. 调用say1、say2、say3、say4方法
- 打印结果
_maybeMask的值是7,_occupied的值是2。- 只打印了
say4、say3两个方法。
- 为什么会有这样的结果呢,后面进行分析。
3. 调用say1、say2、say3
- 打印结果
_maybeMask的值是7,_occupied的值是1。- 只打印了
say3方法。
- 为什么会有这样的结果呢,后面进行分析。
4. 初始化时调用init方法
- 打印结果
init方法是可以正常打印;_maybeMask的值是3,_occupied的值是1。
- 结果分析
init是对象方法;[SSLPerson alloc]=p,所以可以打印。
5. 调用+ (void)sayHeiHei类方法
- 打印结果
_maybeMask、_occupied的值都是0。
- 结果分析
- 因为
sayHeiHei是类方法,存储在元类中,我们打印类的缓存方法是找不到的。
- 因为
为什么 脱离源码分析
- 这份代码可以跑在任何的工程中,不需要源码环境,非常的方便;
lldb调试过程中,遇到问题要重新输出地址,重新跑一遍;- 有的时候源码无法调试,也就没法
lldb调试; - 小规模取样,去除多余代码,可以对底层结构更加的清晰。
四、cache 底层原理分析
_bucketsAndMaybeMask我们可以理解为uintptr_t类型,为8字节,存储这buckets和MaybeMask。- 接下来去探索怎么得到这两个值,我们以
插入为切入点进行分析,也就是写入数据的时候。
insert 函数解析
catch_t中找到insert()函数,和用到的相关函数:
void insert(SEL sel, IMP imp, id receiver);
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
...
mask_t newOccupied = occupied() + 1; // _occupied+1
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;//4
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 {// 4*2 = 8
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; // 4-1=3
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
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);
}
mask_t cache_t::occupied() const
{
return _occupied;
}
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
void cache_t::incrementOccupied()
{
_occupied++;
}
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
第一次插入方法时,进入insert函数:
insert函数传了sel、imp、receiver三个参数;mask_t newOccupied = occupied() + 1;mask_t是32位整形;occupied()就是返回_occupied的值,因为是第一次进入所以函数值为0;- 所以第一次时
newOccupied=0 + 1=1;
if (slowpath(isConstantEmptyCache()))()- 因为是第一次进入,没存过值,所以表达式为
true,进入执行体。
- 因为是第一次进入,没存过值,所以表达式为
if (!capacity) capacity = INIT_CACHE_SIZE;INIT_CACHE_SIZE的值为4,所以capacity = 4。
reallocate(oldCapacity, capacity, /* freeOld */false);- 传入
capacity为4,但是开辟了3字节空间; - 可下面查看
reallocate函数的解析,再回来继续向下看。
- 传入
bucket_t *b = buckets();- 获取当前
buckets容器;
- 获取当前
mask_t m = capacity - 1;m = 4 - 1=3;
mask_t begin = cache_hash(sel, m);- 有三个内存不知道存在哪儿里,通过
cache_hash得到哈希地址,就是开始的存储的位置begin;
- 有三个内存不知道存在哪儿里,通过
- 接下来
do while循环- 无限去找,这个位置有了吗没有,
cache_next()((i+1) & mask)去找下一个; - 进行再哈希,防止哈希冲突,直到不等于
begin,开始插; - 因为是哈希,所以上面打印方法时,
方法不是连续的。
- 无限去找,这个位置有了吗没有,
fastpath(b[i].sel() == 0)- 从来没有进来过,第一次,开始
incrementOccupied()即_occupied++;, - 所以第一次打印
occupied = 1;
- 从来没有进来过,第一次,开始
b[i].set<Atomic, Encoded>(b, sel, imp, cls());- 在合适的位置插入值;
b[i].sel() == sel- 如果已经存在了就不插了;
bad_cache(receiver, (SEL)sel);- 如果
do while循环最终没有找到合适的位置,进行bad_cache。
- 如果
第二次插入方法时,进入insert函数:
mask_t newOccupied = occupied() + 1; // 1+1newOccupied = 1 + 1=2;
unsigned oldCapacity = capacity(), capacity = oldCapacity;capacity = oldCapacity = mask() ? mask()+1 : 0;=3 + 1=4;
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))cache_fill_ratio(capacity):capacity的75%,所以值为3;- 如果超过
75%会进行扩容; 2 + 1 <= 3,表达式成立,执行体什么也没有做。
- 继续执行上面说过的插入操作,不再赘述。
第三次插入方法时,进入insert函数:
mask_t newOccupied = occupied() + 1; // 2+1newOccupied = 2 + 1=3;
unsigned oldCapacity = capacity(), capacity = oldCapacity;capacity = oldCapacity = mask() ? mask()+1 : 0;=3 + 1=4;
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))cache_fill_ratio(capacity):capacity的75%,所以值为3;3 + 1 <= 3,表达式不成立;- 超过了
75%,所以走else进行扩容;
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;- 扩容策略是进行二倍扩容:
capacity = 4 * 2=8;
- 扩容策略是进行二倍扩容:
bucket_t *b = buckets();- 获取当前
buckets容器;
- 获取当前
mask_t m = capacity - 1;m = 8 - 1=7,所以前面打印的时候_maybeMask = 7;
reallocate(oldCapacity, capacity, true);oldCapacity为4、capacity为8;- 进行空间的重新开辟,因为传入了
true,会将旧的空间清空; - 可下面查看
reallocate函数的解析。
- 继续执行上面说过的插入操作,不再赘述。
reallocate 函数解析
reallocate()函数,和用到的相关函数
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);
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
void cache_t::incrementOccupied()
{
_occupied++;
}
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);
}
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);
}
第一次插入方法时,进入reallocate函数:
reallocate(oldCapacity, newCapacity, /* freeOld */false);oldCapacity:旧的内存空间,第一次为0;newCapacity:新的内存空间,第一次为4;freeOld:是否回收旧的内存空间,第一次为false不回收,因为没得回收。
bucket_t *newBuckets = allocateBuckets(newCapacity);- 给
Buckets赋值,开辟新的容器4个内存空间;
- 给
setBucketsAndMask(newBuckets, newCapacity - 1);- 给
Buckets和mask赋值; - 注意:开辟了
4个内存,但是给mask赋值是4 - 1。 - 可下面查看
setBucketsAndMask函数的解析。
- 给
第一次扩容进入时,进入reallocate函数:
reallocate(oldCapacity, newCapacity, /* freeOld */false);oldCapacity:旧的内存空间为4;newCapacity:新的内存空间为8;freeOld:是否回收旧的内存空间,第一次为false不回收;
bucket_t *newBuckets = allocateBuckets(newCapacity);- 给
Buckets赋值,开辟新的容器8个内存空间;
- 给
setBucketsAndMask(newBuckets, newCapacity - 1);- 给
Buckets和mask赋值; - 注意:开辟了
8个内存,但是给mask赋值是8 - 1。 - 可下面查看
setBucketsAndMask函数的解析。
- 给
if (freeOld)- 扩容时为
true,第一次时为false; - 这次扩容为
true,所以走执行体代码。
- 扩容时为
collect_free(oldBuckets, oldCapacity);- 清空
oldBuckets、oldCapacity; - 所以扩容后
say1和say2没有打印;
- 清空
为什么不把之前的say1、say2拿过来呢?
- 之前已经开辟好的内存空间不能修改了,没办法直接追加;
- 如果想把之前内存的数据都赋值到新的内存,消耗性能,缓存数据也会越积越多;
方法越新越好,新方法更重要。
setBucketsAndMask 函数解析
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
#ifdef __arm__
// ensure other threads see buckets contents before buckets pointer
mega_barrier();
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);
// ensure other threads see new buckets before new mask
mega_barrier();
_maybeMask.store(newMask, memory_order_relaxed);
_occupied = 0;
#elif __x86_64__ || i386
// ensure other threads see buckets contents before buckets pointer
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
// ensure other threads see new buckets before new mask
_maybeMask.store(newMask, memory_order_release);
_occupied = 0;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}
第一次插入方法时,进入setBucketsAndMask函数:
setBucketsAndMask(newBuckets, /* newMask */newCapacity - 1);newBuckets:struct bucket_t *类型,是新的容器;newMask:值为4 - 1 = 3,是内存大小 - 1;
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);- 开始存储,将
newBuckets存储在_bucketsAndMaybeMask指针地址里面; buckets的结构是一个哈希表,_bucketsAndMaybeMask中存储这buckets的首地址。
- 开始存储,将
_maybeMask.store(newMask, memory_order_release);- 将
newMask存储在_maybeMask中;
- 将
_occupied = 0;- 因为是新的容器,没有缓存,所以
_occupied = 0;
- 因为是新的容器,没有缓存,所以
哈希函数 解析
哈希相关函数:
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
总结
bucket_t数据结构
sel方法编号,用来找到imp;imp函数指针地址,用来找到函数的实现。
cache_t数据结构
_bucketsAndMaybeMask- 方法缓存列表
buckets,以hash 表的形式存在; buckets的首地址,存储在_bucketsAndMaybeMask中。
- 方法缓存列表
_maybeMask- 记录
buckets的内存空间个数,实际为内存空间个数 - 1。
- 记录
_occupied- 记录
buckets中缓存的方法个数。
- 记录
_flags- 源码中暂时没注意到。
如果帮助到了您,点个👍再走吧😄。