万物皆对象,OC中的类实际上也是一种对象,也就是类对象。
类的真正类型为objc_class
,查看objc_class
是一个结构体,struct objc_class: objc_object
继承于objc_object
。
一、类的结构分析
1. 寻找类的结构
- OC main.m
#import
#import "DZPerson.h"
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
DZPerson *person = [DZPerson alloc];
Class pClass = object_getClass(person);
NSLog(@"%@ - %p",person,pClass);
}
return 0;
}
通过OC对象底层探索 — isa的初始化和指向分析分析可知,打印第一个地址一定是isa
,第二个地址是superclass,它代表的是继承关系,证明了DZPerson
是继承自NSObject
。
我们用
clang
命令将OC代码转化为C++
代码,即.cpp
文件,查看底层:
Dezi$ clang -rewrite-objc main.m -o main.cpp
- C++ main.cpp 关键代码如下:
typedef struct objc_class *Class;
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
DZPerson *person = ((DZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DZPerson"), sel_registerName("alloc"));
Class pClass = object_getClass(person);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kx_p_4mn02529l53n53r5mggdl00000gn_T_main_4dc184_mi_9,person,pClass);
}
return 0;
}
通过上边的C++
代码,我们可以看到pClass
是Class
类型。
而Class
的真正类型是objc_class
。下面我们从源码继续探索objc_class
。
2. 类结构
objc_class 源码:
- 旧版本,在OBJC2中已废弃
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
- 新版本,当前源码使用的版本:找到下面的结构体,我们会发现
objc_class
继承自objc_object
,然后进一步又验证了class
实际上也是一种对象,即类对象。
typedef struct objc_class *Class;
struct objc_class : objc_object {
// Class ISA; // 8
Class superclass; // 8,Class结构体指针
cache_t cache; // 16,cache_t结构体,formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...省略...
}
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
这段代码是objc_class
继承objc_object
的代码,所以objc_class
中也有 isa
结构体。objc_object
和objc_class
之间的关系如下图所示:
3. 类结构成员分析
- Class ISA
隐藏的成员Class继承自父类,是指向元类的指针,占8字节。
struct objc_object {
private:
isa_t isa;
...省略其他的信息...
};
Class superclass
当前类的父类,占8字节。cache_t cache
用于缓存指针和vtable(formerly cache pointer and vtable),占16字节。
truct cache_t {
struct bucket_t *_buckets; // 指针,占8字节
mask_t _mask; // 占4字节
mask_t _occupied; // 占4字节
...省略...
};
- class_data_bits_t bits
是一个结构体,类相关信息在前面三个成员里面都没找到,由此我们分析类的成员变量、方法应该都在这个结构体里面,我们继续探索。
二、探索类的属性存储properties
- 创建DZPerson类,添加成员变量hoppy和属性nickName,添加实例方法sayHello和类方法sayHappy,然后进行断点调试:
@interface DZPerson : NSObject{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
DZPerson *person = [DZPerson alloc];
Class pClass = object_getClass(person);
NSLog(@"%@ - %p", person, pClass);
}
return 0;
}
1. x/4xg打印pClass(可参考Xcode控制台常用调试命令),第一块内存是isa,第二块内存是superclass,第三块内存按照前文应该是cache_t,第四块内存为空
// x/4xg 打印pClass
(lldb) x/4xg pClass
0x1000023b0: 0x001d800100002389 0x0000000100b38140
0x1000023c0: 0x00000001003db260 0x0000000000000000
(lldb)
按照Class结构体的成员变量顺序,以及内存对齐原则,我们用指针偏移的方法,来找一找第四块内存bits,看看bits里边存的到底是什么?
2. 根据首地址用指针偏移找到bits内存地址
首地址为0x1000023b0
,其中isa指针占8字节,superClass占8字节,cache占16字节,按照内存对齐原则,用首地址偏移32个字节得到0x1000023d0
应该就是bits的内容,但是打印结果和我们想的不太一样。
注意:这里需要强转一下,打印得到class_data_bits_t
内存地址:
(lldb) p 0x1000023d0
(long) $1 = 4294976464
(lldb) p (class_data_bits_t *)0x1000023d0
(class_data_bits_t *) $2 = 0x00000001000023d0
3. 根据bits.data()找到bits中存储的内容
struct objc_class : objc_object {
// Class ISA; // 8
Class superclass; // 8
cache_t cache; // 16 不是8 // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
...省略...
}
class_rw_t
是类中存储属性和方法的地方,内部实现返回的是bits.data()
,我们调用一下data
方法得出一个class_rw_t
类型的指针:
(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000100f92f90
我们对指针地址直接取值,结果如下:
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2148139008
version = 0
ro = 0x0000000100002308
methods = {
list_array_tt = {
= {
list = 0x0000000100002240
arrayAndFlag = 4294976064
}
}
}
properties = {
list_array_tt = {
= {
list = 0x00000001000022f0
arrayAndFlag = 4294976240
}
}
}
protocols = {
list_array_tt = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
demangledName = 0x0000000000000000
}
我们可以看到熟悉的methods、properties、protocols等等。
对照源码,进一步验证:
4. 打印properties
(lldb) p $4.properties
(property_array_t) $5 = {
list_array_tt = {
= {
list = 0x00000001000022f0
arrayAndFlag = 4294976240
}
}
}
打印properties,得到property_array_t
,它是一个二维数组,继续打印数组里的值:
(lldb) p $5.list
(property_list_t *) $6 = 0x00000001000022f0
(lldb) p $6.first
(property_t) $7 = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
Fix-it applied, fixed expression was:
$6->first
到这里,我们找到了属性nickName
。
三、探索类的成员变量存储ro
根据上边,我们已经知道properties
存放的是类的属性,结合class_rw_t
里的方法名称,我们尝试探索一下ro部分
1. 探索ro
我们重新运行代码,然后直接取 $3.ro 的值,得到class_ro_t
结构体,我们可以看到ivars
,根据名字成员变量应该在这里边:
(lldb) p $3.ro
(const class_ro_t *) $4 = 0x0000000100002308
(lldb) p *$4
(const class_ro_t) $5 = {
flags = 388
instanceStart = 8
instanceSize = 24
reserved = 0
ivarLayout = 0x0000000100001f89 "\x02"
name = 0x0000000100001f80 "DZPerson"
baseMethodList = 0x0000000100002240
baseProtocols = 0x0000000000000000
ivars = 0x00000001000022a8
weakIvarLayout = 0x0000000000000000
baseProperties = 0x00000001000022f0
}
2. 打印ivars
- 我们找到 ivars 并打印内容,找到
NSString
类型的成员变量hobby
:
(lldb) p $5.ivars
(const ivar_list_t *const) $6 = 0x00000001000022a8
(lldb) p *$6
(const ivar_list_t) $7 = {
entsize_list_tt = {
entsizeAndFlags = 32
count = 2
first = {
offset = 0x0000000100002378
name = 0x0000000100001e64 "hobby"
type = 0x0000000100001fa6 "@\"NSString\""
alignment_raw = 3
size = 8
}
}
}
- 但是我们发现
ivar_list_t
中的count = 2
,而我们只定义了一个成员变量hobby
,所以我们继续往下打印:
(lldb) p $7.get(1)
(ivar_t) $8 = {
offset = 0x0000000100002380
name = 0x0000000100001e6a "_nickName"
type = 0x0000000100001fa6 "@\"NSString\""
alignment_raw = 3
size = 8
}
我们发现了一个_nickName
成员变量,这也印证了 属性会自动生成对应的带下划线的成员变量。
四、探索类的对象方法存储methods
1. 探索methods
按照上边的方法,我们继续打印methods.list
:
(lldb) p $3.methods
(method_array_t) $4 = {
list_array_tt = {
= {
list = 0x0000000100002240
arrayAndFlag = 4294976064
}
}
}
(lldb) p $4.list
(method_list_t *) $5 = 0x0000000100002240
(lldb) p *$5
(method_list_t) $6 = {
entsize_list_tt = {
entsizeAndFlags = 26
count = 4
first = {
name = "sayHello"
types = 0x0000000100001f8b "v16@0:8"
imp = 0x0000000100001b90 (LGTest`-[DZPerson sayHello] at DZPerson.m:13)
}
}
}
通过打印,我们看到定义的对象方法sayHello
存储在 methods 里边。
2. 打印list中的所有方法
我们看到count = 4
,而上边只显示了一个first
,那么继续打印出methods
中的所有方法:
(lldb) p $6.get(1)
(method_t) $7 = {
name = ".cxx_destruct"
types = 0x0000000100001f8b "v16@0:8"
imp = 0x0000000100001c60 (LGTest`-[DZPerson .cxx_destruct] at DZPerson.m:11)
}
(lldb) p $6.get(2)
(method_t) $8 = {
name = "setNickName:"
types = 0x0000000100001f9b "v24@0:8@16"
imp = 0x0000000100001c20 (LGTest`-[DZPerson setNickName:] at DZPerson.h:16)
}
(lldb) p $6.get(3)
(method_t) $9 = {
name = "nickName"
types = 0x0000000100001f93 "@16@0:8"
imp = 0x0000000100001bf0 (LGTest`-[DZPerson nickName] at DZPerson.h:16)
}
由打印我们发现其中并没有我们定义的类方法,而是C++的析构函数destruct
方法(系统默认添加),以及属性 nickName 的setter
和getter
方法。
由此我们印证了 属性的定义,会自动生成setter和getter 而成员变量没有生成setter和getter。
五、探索类中类方法的存储
通过对 bits
的探索,我们找到了属性的存储位置、成员变量的存储位置、实例方法的存储位置,但是唯独没有找到类方法sayHappy
存储在哪里。
1. 在元类中找到类方法
经过验证,类方法其实存储在元类里边,而探索元类,我们就必须知道类的isa指针是指向元类的,通过与上0x00007ffffffffff8ULL
就可以找到元类,这个之前已经做过探索 OC对象底层探索 — isa的初始化和指向分析,此处不再叙说。
(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b38140
0x1000023c0: 0x0000000102121570 0x000000040000000f
(lldb) p/x 0x001d800100002389 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100002388
(lldb) po 0x0000000100002388
DZPerson
// 找到元类之后,继续按照之前找ro的步骤执行
(lldb) x/4gx 0x0000000100002388
0x100002388: 0x001d800100b380f1 0x0000000100b380f0
0x100002398: 0x000000010237f7c0 0x0000000200000003
(lldb) p (class_data_bits_t *)0x1000023a8
(class_data_bits_t *) $9 = 0x00000001000023a8
(lldb) p $9->data()
(class_rw_t *) $13 = 0x000000010237f720
(lldb) p $13->ro
(const class_ro_t *) $15 = 0x00000001000021f8
(lldb) p *$15
(const class_ro_t) $16 = {
flags = 389
instanceStart = 40
instanceSize = 40
reserved = 0
ivarLayout = 0x0000000000000000
name = 0x0000000100001f80 "DZPerson"
baseMethodList = 0x00000001000021d8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000000000000
}
2. 打印baseMethodList
我们继续打印baseMethodList
,至此我们终于找到了类方法sayHappy
。
(lldb) p $16.baseMethodList
(method_list_t *const) $17 = 0x00000001000021d8
(lldb) p *$17
(method_list_t) $18 = {
entsize_list_tt = {
entsizeAndFlags = 26
count = 1
first = {
name = "sayHappy"
types = 0x0000000100001f8b "v16@0:8"
imp = 0x0000000100001bc0 (LGTest`+[DZPerson sayHappy] at DZPerson.m:17)
}
}
}
六、总结
- 万物皆对象,类的本质就是对象,也叫类对象。
- 类和元类是在 编译期 创建的,即在进行alloc操作之前,类和元类就已经被编译器创建出来了。
- 类在 class_rw_t 结构体中存储了编译时确定的成员变量、属性、对象方法、协议等。
- 属性的定义,会 自动生成 对应的 带下划线的成员变量 和 setter、getter 方法。
- 成员变量不会生成 setter 和 getter 方法。
- 对象方法(实例方法) 存储在 类 中,而 类方法 则是 以实例方法形式 存储在 元类 中。
- sel:是类成员方法的指针(是一个方法编号),IMP:是一个保存了方法地址的函数指针。