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+1
newOccupied = 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+1
newOccupied = 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
- 源码中暂时没注意到。
如果帮助到了您,点个?再走吧?。