借助lldb探究OC类结构的底层实现

类结构源码

我们先来看一下runtime中类的结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct objc_object {
private:
isa_t isa;
//下面的函数部分去掉 因为不影响类结构
//...
}
struct objc_class : objc_object {
// Class ISA; // 8字节
Class superclass; // 8字节
cache_t cache; // 16字节 // formerly cache pointer and vtable
class_data_bits_t bits; //这里存的就是类的主要结构 // class_rw_t * plus custom rr/alloc flags
// 具体结构体数据
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
//下面的函数部分去掉 因为不影响类结构
//...
}

从上面的结构体定义可以看出,每一个类的第一个属性都会是一个isa指针,然后是super和cache,之后的bits里面就是类的结构,我们需要使用llbd命令Debug bits内部的内存分配情况,在开始探究之前,我们可以先熟悉下bits中包含的主要数据结构,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
struct class_rw_t {
// 去掉了一些不关心的函数 只留下比较关心的下面这几个数据
// 只读部分
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>()->ro;
}
return v.get<const class_ro_t *>();
}
void set_ro(const class_ro_t *ro) {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
v.get<class_rw_ext_t *>()->ro = ro;
} else {
set_ro_or_rwe(ro);
}
}
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->methods;
} else {
return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->properties;
} else {
return property_array_t{v.get<const class_ro_t *>()->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
}
}
}

可以看到,我们熟悉的函数列表,属性列表和协议列表都在其中,下面我们就通过内存堆栈的Debug来证明下上面的类结构。

知识储备

在开始探索前,我们先来看一下这张图:

这里有一个知识点我们需要了解,一个类(不考虑继承)在内存中会存在3种与它有关联的对象,分别是实例对象,类对象和元类对象,这3种对象通过isa指针进行关联,实例对象可能存在多个,类对象和元类对象是唯一的。其中类对象存储实例方法和属性,元类对象存储类方法。

开始探究

我们先来自定义一个简单的类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@interface LLYModel : NSObject
@property (nonatomic, strong) NSString *name;
- (void)sayHey;
+ (void)sayBey;
@end
@implementation LLYModel {
int _age;
}
- (void)sayHey{}
+ (void)sayBey{}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LLYModel *objc2 = [LLYModel alloc];
objc2.name = @"lly";
NSLog(@"Hello, World! %@",objc2.name);
}
return 0;
}
实例对象

在主函数中我们简单创建一个上面的model实例,然后就可以断点调试了,首先我们查看实例对象的内存地址:

1
2
3
(lldb) x/4gx objc2
0x1006a8a20: 0x001d8001000083ed 0x0000000000000000
0x1006a8a30: 0x0000000100004018 0x0000000000000000

根据对源码的分析,我们知道bits数据存储在首地址偏移32个字节的地方,so我们这样访问bits的地址

1
2
(lldb) p (class_data_bits_t *)0x1006a8a40
(class_data_bits_t *) $94 = 0x00000001006a8a40

拿到bits的地址后,然后访问改结构体的data方法拿到class_rw_t数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(lldb) p $94->data()
(class_rw_t *) $97 = 0x00007fff97ca45c0
(lldb) p *$97
(class_rw_t) $98 = {
flags = 2546615872
witness = 32767
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 140735591042296
}
}
firstSubclass = 0x0000000100346430
nextSiblingClass = 0x0000800000000000
}

然后我们就可以访问该结构内部的相关方法获取数据了,比如方法列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(lldb) p $98.methods()
(const method_array_t) $99 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000102004ee0
arrayAndFlag = 4328541920
}
}
}
(lldb) p $99.list
(method_list_t *const) $100 = 0x0000000102004ee0
(lldb) p *$100
(method_list_t) $101 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 2148007936
count = 0
first = {
name = "\x10"
types = 0x00007fff97ca45c0 "@Fʗ
imp = 0x00007fff8ee94d20 ((void *)0x00007fff8ee959a0: __NSStackBlock)
}
}
}

通过查看内存情况,发现方法列表内部的count为0,说明方法并未存放在实例对象中。

然后看看属性列表

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) p $98.properties()
(const property_array_t) $102 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000001000000000
arrayAndFlag = 68719476736
}
}
}
(lldb) p $102.list
(property_list_t *const) $103 = 0x0000001000000000
(lldb) p *$103
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory

属性列表访问失败,说明也是没有数据的。

类对象

然后我们再来看看类对象中的内存情况,这里我们先拿到类对象的首地址:

1
2
3
(lldb) x/4gx LLYModel.class
0x1000083e8: 0x00000001000083c0 0x000000010034c140
0x1000083f8: 0x00000001006ace60 0x0004802400000007

上面也提到过,类对象是唯一的,所以可以这样访问,其他步骤都差不多就不在重复粘贴,这里只放最后的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(method_list_t) $117 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "sayHey"
types = 0x0000000100003f9a "v16@0:8"
imp = 0x0000000100003da0 (KCObjc`-[LLYModel sayHey])
}
}
}
(lldb) p $117.get(0)
(method_t) $118 = {
name = "sayHey"
types = 0x0000000100003f9a "v16@0:8"
imp = 0x0000000100003da0 (KCObjc`-[LLYModel sayHey])
}
(lldb) p $117.get(1)
(method_t) $119 = {
name = ".cxx_destruct"
types = 0x0000000100003f9a "v16@0:8"
imp = 0x0000000100003e00 (KCObjc`-[LLYModel .cxx_destruct])
}
(lldb) p $117.get(2)
(method_t) $120 = {
name = "name"
types = 0x0000000100003f87 "@16@0:8"
imp = 0x0000000100003db0 (KCObjc`-[LLYModel name])
}
(lldb) p $117.get(3)
(method_t) $121 = {
name = "setName:"
types = 0x0000000100003f8f "v24@0:8@16"
imp = 0x0000000100003dd0 (KCObjc`-[LLYModel setName:])
}

除了我们定义的实例方法外,还看到了属性的get和set方法,还有一个编译器默认添加的析构方法。接着是属性列表

1
2
3
4
5
6
7
(property_list_t) $124 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 1
first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
}
}

只有一个我们定义的name属性。

元类对象

最后我们来看看元类的内存分配情况,首先我们拿到元类的首地址:

1
2
3
(lldb) x/4gx objc_getMetaClass(object_getClassName(objc2))
0x1000083c0: 0x000000010034c0f0 0x000000010034c0f0
0x1000083d0: 0x000000010200d880 0x0004e03500000007

方法列表:

1
2
3
4
5
6
7
8
9
10
11
(method_list_t) $135 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "sayBey"
types = 0x0000000100003f9a "v16@0:8"
imp = 0x0000000100003d90 (KCObjc`+[LLYModel sayBey])
}
}
}

元类中存放了我们上面定义的一个类方法

属性列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) p $132.properties()
(const property_array_t) $136 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
(lldb) p $136.list
(property_list_t *const) $137 = 0x0000000000000000
(lldb) p *$137
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory

元类中没有存放属性。

小结

通过以上的探索过程,证明了我们之前的结论其中类对象存储实例方法和属性,元类对象存储类方法。,通过源码+lldb相结合的方式,可以更好的证明我们已知的一些结论。也给我们探索更广阔的空间提供了可能。