在上一篇文章中我们已经探究了isa和superclass的指向问题,本文将通过lldb调试,探索objc_class
中bit的存储信息。
探索原理
我们先看下objc_class的结构。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
...
}
我们知道objc_class
的首地址是由isa开始,而之后依次是superclass、cache、bits,如果我们要访问探究bits的内存,那么只需要将objc_class的地址加上isa、superclass和cache占用内存大小的和,就可以访问到bits的空间。
isa和superclass本质都是指针类型,占用8个字节,而cache_t占用16个字节的空间,所以objc_class地址偏移32个字节的大小,就可以得到bits的地址。
关于cache_t的大小可以通过查看它的结构得知
cache_t中决定大小只以下几个成员(其余静态变量不影响大小)struct cache_t { ... explicit_atomic<struct bucket_t *> _buckets; explicit_atomic<mask_t> _mask; ... uint16_t _flags; ... uint16_t _occupied; ... }
_buckets是指针类型,占用8个字节,mask_t实际是uint32_t类型,占用4个字节,_flags和_occupied均是uint16_t,占用2个字节,所以cache_t占用8+4+2+2=16个字节
本文探究需要借助可以编译的objc4-7.8.1源码
探究准备
与上次一样,本文继续采用Animal和Cat类来探究,不同的是,我们在Cat中添加属性和方法。
@interface Cat : Animal {
NSString *name;
}
@property(nonatomic, strong) NSString *ownName;
@end
@implementation Cat
- (void)eat {
}
+ (void)run {
}
@end
在main方法中打上断点,开始探究
探究过程
进入断点后,执行p/x Cat.class
:
(lldb) p/x Cat.class
(Class) $0 = 0x0000000100002268 Cat
给Cat的地址加上偏移量0x20,p/x查看
(lldb) p/x 0x0000000100002288
(long) $1 = 0x0000000100002288
转化为class_data_bits_t
(lldb) p (class_data_bits_t *)$1
(class_data_bits_t *) $2 = 0x0000000100002288
通过data()获取bits
(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000100862150
查看class_rw_t
的结构,可以看到它有ro
、methods
、properties
和protocols
几个重要方法,我们通过lldb一步步探究。
(lldb) p/x *$3
(class_rw_t) $4 = {
flags = 0x80080000
witness = 0x0001
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = 0x0000000100002088
}
firstSubclass = nil
nextSiblingClass = nil
}
properties
首先查看properties
保存了哪些内容。
(lldb) p $4.properties()
(const property_array_t) $5 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000100002180
arrayAndFlag = 4294975872
}
}
继续查看list
(lldb) p $5.list
(property_list_t *const) $6 = 0x0000000100002180
(lldb) p *$6
(property_list_t) $7 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 1
first = (name = "ownName", attributes = "T@\"NSString\",&,N,V_ownName")
}
}
可以看到properties
数量只有一个ownName,而成员变量name并不在其中,所以我们可以得出结论,properties保存属性(property)。
ro
同样查看ro的内容
(lldb) p $4.ro()
(const class_ro_t *) $8 = 0x0000000100002088
(lldb) p *$8
(const class_ro_t) $9 = {
flags = 388
instanceStart = 8
instanceSize = 24
reserved = 0
ivarLayout = 0x0000000100000f18 "\x02"
name = 0x0000000100000f14 "Cat"
baseMethodList = 0x00000001000020d0
baseProtocols = 0x0000000000000000
ivars = 0x0000000100002138
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000100002180
_swiftMetadataInitializer_NEVER_USE = {}
}
查看ivars
(lldb) p $9.ivars
(const ivar_list_t *const) $10 = 0x0000000100002138
(lldb) p *$10
(const ivar_list_t) $11 = {
entsize_list_tt<ivar_t, ivar_list_t, 0> = {
entsizeAndFlags = 32
count = 2
first = {
offset = 0x0000000100002238
name = 0x0000000100000f25 "name"
type = 0x0000000100000f61 "@\"NSString\""
alignment_raw = 3
size = 8
}
}
}
可以看到Cat的ivars有两个,通过索引查看
(lldb) p $11.get(0)
(ivar_t) $12 = {
offset = 0x0000000100002238
name = 0x0000000100000f25 "name"
type = 0x0000000100000f61 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $11.get(1)
(ivar_t) $13 = {
offset = 0x0000000100002230
name = 0x0000000100000f2a "_ownName"
type = 0x0000000100000f61 "@\"NSString\""
alignment_raw = 3
size = 8
}
可以看,ivars中不仅存放了成员变量name,而且经过@property修饰的属性也会在变量名前加上"_"保存在ivar中。
methods
查看methods的内容
(lldb) p $4.methods()
(const method_array_t) $14 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000020d0
arrayAndFlag = 4294975696
}
}
}
(lldb) p $14.list
(method_list_t *const) $15 = 0x00000001000020d0
(lldb) p *$15
(method_list_t) $16 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "eat"
types = 0x0000000100000f59 "v16@0:8"
imp = 0x0000000100000e10 (KCObjc`-[Cat eat])
}
}
}
可以看到methods中保存了4个方法信息,可以通过索引查看具体内容
(lldb) p $16.get(0)
(method_t) $17 = {
name = "eat"
types = 0x0000000100000f59 "v16@0:8"
imp = 0x0000000100000e10 (KCObjc`-[Cat eat])
}
(lldb) p $16.get(1)
(method_t) $18 = {
name = "ownName"
types = 0x0000000100000f6d "@16@0:8"
imp = 0x0000000100000e20 (KCObjc`-[Cat ownName])
}
(lldb) p $16.get(2)
(method_t) $19 = {
name = "setOwnName:"
types = 0x0000000100000f75 "v24@0:8@16"
imp = 0x0000000100000e40 (KCObjc`-[Cat setOwnName:])
}
(lldb) p $16.get(3)
(method_t) $20 = {
name = ".cxx_destruct"
types = 0x0000000100000f59 "v16@0:8"
imp = 0x0000000100000e80 (KCObjc`-[Cat .cxx_destruct])
}
可以看到methods中保存了实例方法eat,ownName的set和get方法(编译器生成的),还有一个析构方法,但是,类方法run却不在其中。
实际上,对象的实例方法保存在类对象,而属于类对象的类方法,是保存在类对象的类--元类中的,我们可以通过同样的过程验证。
验证类方法的保存位置
我们通过类的isa找到元类,用同样的指针偏移找到类方法
// 查看Cat类信息
(lldb) x/4gx Cat.class
0x100002268: 0x0000000100002240 0x00000001000022b8
0x100002278: 0x00000001008621f0 0x0001802400000003
// 获取元类地址
(lldb) p/x 0x0000000100002240 & 0x00007ffffffffff8ULL
(unsigned long long) $22 = 0x0000000100002240
(lldb) p/x 0x0000000100002260
(long) $23 = 0x0000000100002260
// 元类地址加上0x20得到class_data_bits_t
(lldb) p (class_data_bits_t *)$23
(class_data_bits_t *) $24 = 0x0000000100002260
// 获取class_rw_t
(lldb) p $24->data()
(class_rw_t *) $25 = 0x00000001008161a0
(lldb) p *$25
(class_rw_t) $26 = {
flags = 2684878849
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = 4294975520
}
firstSubclass = nil
nextSiblingClass = nil
}
// 获取methods
(lldb) p $26.methods()
(const method_array_t) $27 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100002068
arrayAndFlag = 4294975592
}
}
}
(lldb) p $27.list
(method_list_t *const) $28 = 0x0000000100002068
// 打印method信息
(lldb) p *$28
(method_list_t) $29 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "run"
types = 0x0000000100000f59 "v16@0:8"
imp = 0x0000000100000e00 (KCObjc`+[Cat run])
}
}
}
猜想得证。
总结
本文主要对objc_class中bits保存信息的一个探究,结论如下
- 类对象的bits保存了类的属性、成员变量、方法、协议等信息
- 实例方法保存在类对象中,具体在class_rw_t的methods中
- 元类对象的bits保存类方法,具体在class_rw_t的methods中
- property修饰的属性会生成set和get方法,属性信息保存在properties中,通过会生成一个“_属性名”保存在ro的ivars中
- 成员变量保存在ro的ivars中
网友评论