前言
通过前两篇文章 OC - 方法的本质(上) 和 OC - 方法的本质(下),我们对类有了一定的了解。而且我们也分别对类
里面的 isa
、superclass
、bits
进行了分析探究,还有 cache
没有进行分析。今天我们就对类
的 cache
进行分析和探究。
cache_t 数据结构
通过前两篇文章,我们已经知道如何还原 bits
中的数据了。接下来我们试着还原 cache
中的数据。
(lldb) p/x TPerson.class
(Class) $0 = 0x0000000100008708 TPerson
(lldb) p (cache_t *)0x0000000100008718
(cache_t *) $1 = 0x0000000100008718
(lldb) p *$1
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4298515392
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 0
}
}
_flags = 32820
_occupied = 0
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0000803400000000
}
}
}
}
// 然后读取里面的值。
(lldb) p $2._bucketsAndMaybeMask
(explicit_atomic<unsigned long>) $3 = {
std::__1::atomic<unsigned long> = {
Value = 4298515392
}
}
(lldb) p $3.Value
error: <user expression 4>:1:4: no member named 'Value' in 'explicit_atomic<unsigned long>'
$3.Value
~~ ^
(lldb) p $2._maybeMask
(explicit_atomic<unsigned int>) $4 = {
std::__1::atomic<unsigned int> = {
Value = 0
}
}
(lldb) p $4.Value
error: <user expression 6>:1:4: no member named 'Value' in 'explicit_atomic<unsigned int>'
$4.Value
~~ ^
(lldb) p $2._originalPreoptCache
(explicit_atomic<preopt_cache_t *>) $5 = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0000803400000000
}
}
(lldb) p $5.Value
error: <user expression 8>:1:4: no member named 'Value' in 'explicit_atomic<preopt_cache_t *>'
$5.Value
~~ ^
(lldb)
可以看到在cache_t
中有 _bucketsAndMaybeMask
、_maybeMask
、_flags
、_occupied
、_originalPreoptCache
数据,但是都无法打印它们的Value
。那么这些数据分别代表什么意思呢?接下来我们就一起看看底层源码( 苹果objc源码 和 objc源码编译 ) 的描述和处理。
在 cache_t
中,我可以清晰的开到 _bucketsAndMaybeMask
、_maybeMask
、_flags
、_occupied
、_originalPreoptCache
的结构。
那么 cache_t
的缓存在哪里呢?IMP
和SEl
在哪里呢?接下来我们带着这些疑问继续查看源码。那么应该怎么去分析源码呢?既然 cache_t
是用来缓存数据的,那么它一定会有 增
、删
、改
、查
的方法。我们可以通过查看 cache_t
中提供的 方法
。
看到这里,出现了一个
bucket_t
。emptyBuckets()
清空 buckets
,allocateBuckets()
开辟 buckets
,emptyBucketsForCapacity()
,endMarker()
,bad_cache()
。
继续往下看
发现这里有一个
insert
插入方法。接下来我们就看下这个 insert
方法是如何实现的。
发现在
insert
方法中,对 bucket_t
进行了操作。得出一个结论:在 cache_t
的核心就是 bucket_t
。
接下来我们就看看 bucket_t
。
当我们点击进入
bucket_t
源码时,第一眼就看到了我们一直在寻找的 IMP
和SEl
。就得到如下这张 cache_t
的数据结构图。
cache_t 分析
我们对 cache_t
数据结构有了一定了解。接下来我们就一起分析一下 cache_t
。
1. 通过 lldb
分析
根据我们之前对 bits
的分析思路。而且我们也已经在上文中拿到了 cache_t
,那么如果拿到 IMP
和SEl
呢?结合 cache_t
的数据结构图,我开始对 cache_t
进行分析。
(lldb) p [tp formatPerson]
2021-06-23 14:13:32.768517+0800 KCObjcBuild[5296:231526] --- TPerson -- formatPerson ---
(lldb) p/x tClass
(Class) $0 = 0x0000000100008780 TPerson
(lldb) p (cache_t *)(0x0000000100008780+0x10)
(cache_t *) $1 = 0x0000000100008790
(lldb) p *$1
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4326470912
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7
}
}
_flags = 32816
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0001803000000007
}
}
}
}
(lldb) p $2.buckets()
(bucket_t *) $3 = 0x0000000101e0b500
(lldb) p *$3
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 48368
}
}
}
我们已经分析得到了 bucket_t
了, 那如果获取 bucket_t
中的 IMP
和SEl
呢?继续查 bucket_t
的源码。
发现 bucket_t
提供了 sel()
、imp()
、rawImp()
方法。接下来我们依次验证。
(lldb) p $4.sel()
(SEL) $15 = "formatPerson"
(lldb) p $4.imp(nil, TPerson.class)
(IMP) $16 = 0x0000000100003b70 (KCObjcBuild`-[TPerson formatPerson])
这里有一个点需要注意:imp
需要传入一个 UNUSED_WITHOUT_PTRAUTH bucket_t *base
的参数。查看宏定义得知这个参数可以设置为 nil
。
这里有一个点需要注意:
p $2.buckets()
时,有时候是无法获取到值的。是因为 buckets()
是一个集合 哈希函数
,因为 哈希函数
内部元素的位置是随机的。可以通过 p $2.buckets()[1]
索引方式去取。这里笔者为什么通过p $2.buckets()
就把值获取出来呢?也许是因为在一开始的时候就已经执行了 p [tp formatPerson]
方法。或者当输出如下格式的时候,就找到了 bucket_t
的值。笔者也曾在p $2.buckets()[6]
的时候才获取到值。所以大家要有耐心和细心!
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 48368
}
}
}
2. 通过 脱离源码
分析
当我们在没有源码的情况下,或者有源码但是并不能调试的情况下,那么我们应该怎么进行分析呢?既然源码没法进行编译,那么我们就在自己的工程中,仿照源码写一份能进行编译的代码。
我们已经知道 objc_class
的数据结构,那么我们就自定义一个tcd_objc_class
。
这里有一个需要注意的点:
objc_class
中有一个隐藏参数 isa
,这里需要一一对应。如果没有添加会导致后续输出 cache
数据不对。
objc_class
中有一个 cache_t
,我们也自定义一个 tcd_cache_t
。
objc_class
中有一个 class_data_bits_t
,我们也自定义一个 tcd_class_data_bits_t
。
在 cache_t
中有一个 bucket_t
,我们也自定义一个 tcd_bucket_t
。
objc_class
中需要的,我们都已经自定义完成。完整代码如下:
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct tcd_bucket_t {
SEL _sel;
IMP _imp;
};
struct tcd_cache_t {
uint16_t _bucjetsAndMaybeMask; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
struct tcd_class_data_bits_t {
uintptr_t bits;
};
// cache class
struct tcd_objc_class {
Class isa;
Class superclass;
struct tcd_cache_t cache; // formerly cache pointer and vtable
struct tcd_class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
};
所有需要的东西我们已经全部准备好了,就下来我们就一起看看怎么使用吧!
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
TPerson * tp = [TPerson alloc];
Class tpClass = tp.class;
[tp formatPerson0];
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
定义好了 tp
,得到了 tpClass
。然后我们就需要把 tpClass
(objc_class
类型)的强转成我们定义的 tcd_objc_class
类型。
struct tcd_objc_class *tcd_class = (__bridge struct tcd_objc_class *)(tpClass);
接下来我们就打印一下 tcd_class
的 cache
。
输出:
NSLog(@"--- TPerson -- formatPerson --- : %@",tcd_class->cache);
打印:
--- TPerson -- formatPerson --- : -[TPerson formatPerson0]
接着我们输出 _maybeMask
和 _occupied
输出:
NSLog(@"-- _maybeMask : %u -- _occupied : %u ", tcd_class->cache._maybeMask, tcd_class->cache._occupied);
打印:
-- _maybeMask : 3 -- _occupied : 1
发现 _occupied
的值为 1
,_maybeMask
的值为 3
,3
号位置。这样我们通过 lldb
分析打印的结果很相似了。
我们在 lldb
分析的时候得知数据是存储在 buckets
的,所以这里我将 tcd_cache_t
的结构调整一下。tcd_bucket_t
替换 _bucjetsAndMaybeMask
。tcd_bucket_t
代码调整如下:
struct tcd_cache_t {
struct tcd_bucket_t *_bukets; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
我们知道 buckets
是一个哈希函数
的集合,所有接下来我们就通过 for
循环打印一下结果。
TPerson * tp = [TPerson alloc];
Class tpClass = tp.class;
[tp formatPerson0];
struct tcd_objc_class *tcd_class = (__bridge struct tcd_objc_class *)(tpClass);
NSLog(@"-- _maybeMask : %u -- _occupied : %u ", tcd_class->cache._maybeMask, tcd_class->cache._occupied);
for (mask_t i = 0; i < tcd_class->cache._occupied; i++) {
struct tcd_bucket_t bucket = tcd_class->cache._bukets[i];
NSLog(@"-- %@ -- %p",NSStringFromSelector(bucket._sel), bucket._imp);
}
这里并没有得到我们想要的数据,那我们再换个方式打印。
for (mask_t i = 0; i < tcd_class->cache._maybeMask; i++) {
struct tcd_bucket_t bucket = tcd_class->cache._bukets[i];
NSLog(@"-- %@ -- %pf",NSStringFromSelector(bucket._sel), bucket._imp);
}
发现在最后一个打印了我们想要的数据。那么是为什么呢?首先
occupied
表示里面存储了几个,我们这里只有1
个,maybemask
表示总共开辟了多少内存,我们这里开辟了3
个内存。其次buckets
是一个哈希函数
集合,所以值并不一定在第一个。这里笔者打印时,数据就在最后一个。
接下来我们继续尝试调用多个方法。
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
TPerson * tp = [TPerson alloc];
Class tpClass = tp.class;
[tp formatPerson0];
[tp formatPerson1];
[tp formatPerson2];
[tp formatPerson3];
struct tcd_objc_class *tcd_class = (__bridge struct tcd_objc_class *)(tpClass);
NSLog(@"-- _maybeMask : %u -- _occupied : %u ", tcd_class->cache._maybeMask, tcd_class->cache._occupied);
for (mask_t i = 0; i < tcd_class->cache._maybeMask; i++) {
struct tcd_bucket_t bucket = tcd_class->cache._bukets[i];
NSLog(@"-- %@ -- %pf",NSStringFromSelector(bucket._sel), bucket._imp);
}
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
在这个地方调用了 formatPerson0~3
四个方法。我们来看看打印结果呢?
根据打印结果我们可以得知:开辟了
7
个内存,但是只存储了 2
个。打印了方法 formatPerson3
和 formatPerson2
,formatPerson0
和 formatPerson1
方法去哪了呢?接下来我们就在 底层原理分析 中一探究竟。
3. 底层原理分析
首先我们先在cache_t
源码中找到 install
方法。
方法 void insert(SEL sel, IMP imp, id receiver);
中有我们比较熟悉的参数:SEL sel
和IMP imp
还有一个消息接收者id receiver
。接下来我们就看看insert
方法是怎么处理的。
1. 计算容量
occupied()
初始化:第一次进入是 occupied()
没有值为 0
,插入 1
,newOccupied = 1
。
2. 开辟容量
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
即:capacity = 4
;但是我们再上述分析过程中 得知 capacity = 3
啊。接下来我们看 reallocate
里做了什么操作。
在
reallocate
中做了三件事:1. allocateBuckets()
开辟 buckets
内存;2.通过 setBucketsAndMask
设置 buckets
和 mask
的值;3. 由freeOld
控制是否 collect_free
释放内存。接下来我们依次查看它们分别都做了什么。
allocateBuckets()
在
allocateBuckets()
中主要做了两件事:1. bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
开辟大小;2.end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
给 SEL
和 IMP
赋值。
setBucketsAndMask
在
setBucketsAndMask
中主要根据不同的架构系统向_bucketsAndMaybeMask
和 _maybeMask
写入数据。
collect_free
在
collect_free
中,主要是清空数据,回收内存。
3. 哈希存值
buckets
通过 cache_hash(sel, m)
的开始位置,然后 do while
循环查找,为了防止哈希冲突
进行了 cache_next(i, m)
再哈希
。
4. 3/4 容积
超过75%
进行扩容,低于75%
正常插入。
超出
75%
,进行capacity * 2
2倍扩容
。扩容的最大容量不会超过mask
最大值的 2^15
4. insert 调用流程
在 insert
处,添加断点。
insert
调用栈:_objc_msgSend_uncached
-> lookUpImpOrForward
-> log_and_fill_cache
-> insert
。
但是我们并不知道[tp formatPerson]
到 _objc_msgSend_uncached
是一个什么样的过程。接下来我们可以通过 汇编
的方式进行查看。
在 汇编
代码中,我们可以看到 [tp formatPerson]
的底层实现是通过 objc_msgSend
方法实现的。
这里我们就知道了 insert
方法的整个流程。[tp formatPerson]
-> objc_msgSend
-> _objc_msgSend_uncached
-> lookUpImpOrForward
-> log_and_fill_cache
-> insert
。
附件:cache_t 流程图
补充:
1. 处理器适配架构
真机:ARM64
、模拟器:i386
、电脑:x86_64
总结
通过 lldb
结合源码调试,我们知道方式是存储在 cache
中,方法的 IMP
和 SEL
存储在 bucket
中。然后我们又了解了仿照objc_class
源码,自定义tcd_objc_class
的方法进行调试。发现这种方法调用简单,操作方便。接下来我们有对cache_t
源码进行了分析,了解的方法的存储过程。知道了 [tp formatPerson]
在底层代码中是怎样调用的。