类的结构和定义
- 首先跟踪源码,找到Class的的定义,发现其本质为objc_class类型的指针,并且 objc_class继承自objc_object,其中objc_class中有一个隐藏的isa指针,最后在objc_object中发现了isa的定义
这里要注意的是,在new版本的源码中,objc_class继承自objc_object,在之前的旧版本中,isa指针直接定义在objc_class中,其中OC中的NSObject在编译到底层的时候都会转变成相应的结构体objc_object
// 旧的版本,在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 *` */
// 新的版本,也是现在源码编译调试使用的版本
typedef struct objc_class *Class;
// objc_class定义
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();
}
// 下面都是方法
}
// objc_object定义
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
2 类的成员的存储
回到objc_class,其内部定义了四个成员如下
- Class isa
- Class superclass
- cache_t cache
- class_data_bits_t bits
- 首先我们知道isa的指针是关联对象和类,superClass指向继承类,那么类的成员能够存储的地方就只有cache和bits
- 先看一下cache的结构体定义(不是一个结构体指针,是一个结构体),其中 mask_t为固定的4字节类型的值,而bucket_t则是一个8字节的指针,都不能存放我们定义的属性值,所以可以排除cache,这里也看出 cache的内存大小只有4+4+8=16字节(64位下)
struct cache_t {
struct bucket_t *_buckets; // 8
mask_t _mask; // 4
mask_t _occupied; // 4
}
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
}
- 这里我们可以大胆猜测,bits中是否存放有我们定义的成员以及方法
2.1属性的存储探索
- 首先为LGPerson新建一个成员变量hobby以及属性nickName,并且添加了示例方法和类方法,下面开始代码断点
@interface LGPerson : NSObject{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
Class pClass = object_getClass(person);
NSLog(@"%@ - %p",person,pClass);
}
return 0;
}
- x/4gx 打印pClass的内容,但是除了第一以及第二的内存,是我们熟悉的isa以及superClass指针以外,第三块地址的内容我们完全不知晓,第四块地址直接就不存在
- 按照Class结构体的成员定义顺序,以及内存对齐原则,我们尝试用指针偏移的方法,来找到第四块地址bits的所在,并且看看bits存放的内容到底为何
// 第一步
(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000
(lldb)
- 首地址为0x1000023b0,其中isa指针占用8字节,superClass占用8字节,cache占用了16字节(上面计算过),那么按照内存对齐原则,我们用首地址偏移32个字节,就应该能得出bits的内容 ,偏移后得出0x1000023d0为bits的理论地址,但是打印结果很迷茫,这里我们强转一下,再次打印,终于打印出class_data_bits_t的结构体
// 第二步
(lldb) po 0x1000023d0
objc[1017]: mutex incorrectly locked
4294976464
(lldb) p (class_data_bits_t *)0x1000023d0
(class_data_bits_t *) $2 = 0x00000001000023d0
- class_rw_t为类中存储属性和方法的地方,看一下class_rw_t的实现,返回的是bits.data(),我们这里调用一下data方法之后得出一个class_rw_t类型的指针,直接取值,结果如下
// 第三步
(lldb) p $2->data()
(class_rw_t *) $4 = 0x0000000100fd3150
(lldb) p *$4
(class_rw_t) $5 = {
flags = 2148139008
version = 0
ro = 0x0000000100002308
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100002240
arrayAndFlag = 4294976064
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x00000001000022f0
arrayAndFlag = 4294976240
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
demangledName = 0x0000000000000000
}
- 使用p命令查看结果中properties的值,此时出现了新的类型property_array_t(在源码的objc-runtime-new.h中有其定义),为一个二维数组,继续探索其内部list,进行 p $6.list,此时出现property_list_t类型,继承自entsize_list_tt,在其内部发现first方法,尝试打印,最后找到了属性nickName
(lldb) p $5.properties
(property_array_t) $6 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x00000001000022f0
arrayAndFlag = 4294976240
}
}
}
(lldb) p $6.list
(property_list_t *) $7 = 0x00000001000022f0
(lldb)
(lldb) p $7.first
(property_t) $8 = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
Fix-it applied, fixed expression was:
$7->first
(lldb)
// entsize_list_tt and property_list_t
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element first;
}
2.2 成员变量的存储
- 为了直观的体现,我们先重新编译运行一下项目,断点打上,先直接跳到输出class_rw_t
(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000
(lldb) p (class_data_bits_t *)0x1000023d0
(class_data_bits_t *) $1 = 0x00000001000023d0
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000102139a80
(lldb) p *$2
(class_rw_t) $3 = {
flags = 2148139008
version = 0
ro = 0x0000000100002308
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100002240
arrayAndFlag = 4294976064
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x00000001000022f0
arrayAndFlag = 4294976240
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
demangledName = 0x0000000000000000
}
- 我们已经知道properties存放的是类的属性,结合class_rw_t里的方法名称,可以先尝试探索一下ro部分
- 先p出ro的地址,得出一个class_ro_t类型的结构体指针,我们直接取值,拿到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 "LGPerson"
baseMethodList = 0x0000000100002240
baseProtocols = 0x0000000000000000
ivars = 0x00000001000022a8
weakIvarLayout = 0x0000000000000000
baseProperties = 0x00000001000022f0
}
(lldb)
- 继续寻找ivar的值,打印得到ivar_list_t类型的指针,依旧取值,输出内容,发现了我们定义的成员变量hobby
(lldb) p $5.ivars
(const ivar_list_t *const) $7 = 0x00000001000022a8
(lldb) p *$7
(const ivar_list_t) $8 = {
entsize_list_tt<ivar_t, ivar_list_t, 0> = {
entsizeAndFlags = 32
count = 2
first = {
offset = 0x0000000100002378
name = 0x0000000100001e64 "hobby"
type = 0x0000000100001fa6 "@\"NSString\""
alignment_raw = 3
size = 8
}
}
}
(lldb)
- 到这里,成员变量存放的位置我们也已经找到,但是我们只是定义了一个hobby属性,但是count显示个数为2,我们用get方法拿到剩余的一个值_nickName,这里也证明了属性的定义会自动生成对应的成员变量
(lldb) p $8.get(1)
(ivar_t) $9 = {
offset = 0x0000000100002380
name = 0x0000000100001e6a "_nickName"
type = 0x0000000100001fa6 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb)
2.3 方法的存储
依然按照上面寻找成员变量存储位置之时运行的代码,从打印出$3,也就是class_rw_t的结构开始进行方法的查找,可以发现,class_rw_t中的properties代表着属性的存储,ro代表着成员变量的存储,那么可以推断,methods则应该存放类的方法
先执行 p $3.methods方法,获得一个method_array_t类型的结构体,打印出其中的list地址,并且取值得到一个entsize_list_tt,内部的第一个元素存放着我们的sayHello方法
(lldb) p $3.methods
(method_array_t) $12 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100002240
arrayAndFlag = 4294976064
}
}
}
(lldb) p $12.list
(method_list_t *) $13 = 0x0000000100002240
(lldb) p *$13
(method_list_t) $14 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "sayHello"
types = 0x0000000100001f8b "v16@0:8"
imp = 0x0000000100001b90 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
}
}
}
(lldb)
- 但是可以发现,其数组个数count=4,也就是除了我们定义的sayHello方法之外还有另外的三个方法,输出打印其他三个方法名称依次为C++的析构函数destruct方法,属性nickName的getter和setter方法
(lldb) p $14.get(1)
(method_t) $15 = {
name = ".cxx_destruct"
types = 0x0000000100001f8b "v16@0:8"
imp = 0x0000000100001c60 (LGTest`-[LGPerson .cxx_destruct] at LGPerson.m:11)
}
(lldb) p $14.get(2)
(method_t) $16 = {
name = "nickName"
types = 0x0000000100001f93 "@16@0:8"
imp = 0x0000000100001bf0 (LGTest`-[LGPerson nickName] at LGPerson.h:17)
}
(lldb) p $14.get(3)
(method_t) $17 = {
name = "setNickName:"
types = 0x0000000100001f9b "v24@0:8@16"
imp = 0x0000000100001c20 (LGTest`-[LGPerson setNickName:] at LGPerson.h:17)
}
- 至此,我们已经探索出了类的示例方法存放在类的class_rw_t结构体中的methods里面
2.4 类方法的存储
通过上面的步骤,我们已经可以了解到类的实例方法的存储,但是并没有发现类方法sayHello的存储,通过class_rw_t结构体内部的名称分析,基本可以判断并没有适合存放类方法的位置
那么再回到Class的基本结构成员,isa,superClass,cache,bits四个成员,其中bits.data就是我们一直在寻找的class_rw_t结构体,已经证明其内部不可能存放类方法,cache内部的8字节的bucket_t指针存放的是key和imp的键值对,和我们了解的方法的存储结构并不一样,所以我们暂时先跳过。而superClass则是其父类,LGPerson的父类为NSObject,但是NSObject内部并没有sayHello方法,所以也可以排除在外
最后剩下isa指针,在之前的文章中isa指针走向
,我们探索过了isa指针的走向,了解到了类的isa指针,指向的是一个同名类,我们把它叫做元类,那么类方法会不会保存在元类中,我们测试一下lldb控制台输入命令 x/4gx pClass之后,先通过isa指针查找到LGPerson的元类
(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000
(lldb) p/x 0x001d800100002389&0x00007ffffffffff8ULL
(unsigned long long) $19 = 0x0000000100002388
(lldb) po 0x0000000100002388
LGPerson
(lldb)
- 因为元类也是类的一种,也是继承自NSObject的一种特殊结构,所以我们也可以依旧按照对类的查找方法来进行元类的结构探索,其中元类的地址为0x0000000100002388,依次找出class_data_bits_t,通过->data()方法找到class_rw_t结构体,打印出里面的methods,获取其中的list数组,最后找到了我们定义的类方法sayHappy流程和查找类的实例方法一样,所以直接看结果
(lldb) p $26.list
(method_list_t *) $27 = 0x00000001000021d8
(lldb) p *$27
(method_list_t) $28 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "sayHappy"
types = 0x0000000100001f8b "v16@0:8"
imp = 0x0000000100001bc0 (LGTest`+[LGPerson sayHappy] at LGPerson.m:17)
}
}
}
(lldb)
总结
- 类之间除了基本的继承关系之外,还依靠isa指针进行对象和类的关联,也就是*对象-类-元类-根源类-根源类这一组isa关系图
- 其中类的属性和成员变量都存放在类的class_rw_t结构体中
- 属性的定义,还伴随着成员变量以及其getter和setter的自动生成
- 类的类方法,则以实例方法的形式,存放在元类中,而元类又是继承自NSObject,形成一个闭环
- 至此,类的基本结构以及其成员变量,属性和方法的存储也基本探索清楚了,如果有失误或者补足的地方,还望留言一起讨论~