上一篇: iOS底层原理03 - 对象的本质与isa
下一篇:iOS底层原理05 - 属性关键字copy&weak&strong底层分析
0. 补充: 内存平移
在看类的结构之前,先了解一下什么是内存平移。
int a[4] = {1, 2, 3, 4};
NSLog(@"&a=%p, &a[0]=%p, &a[1]=%p", &a, &a[0], &a[1]);
// 打印结果:
// &a=0x7ffeefbff460, &a[0]=0x7ffeefbff460, &a[1]=0x7ffeefbff464
NSString *b[4] = {@"1", @"2", @"3", @"4"};
NSLog(@"&b=%p, &b[0]=%p, &b[1]=%p", &b, &b[0], &b[1]);
// 打印结果:
// &b=0x7ffeefbff440, &b[0]=0x7ffeefbff440, &b[1]=0x7ffeefbff448
通过打印结果可以看出:
- a、b的首地址和第一个元素的指针地址是一致的
- a的第二个元素与第一个元素地址之前相差4个字节,即int类型所占内存;b的第二个元素与第一个元素相差8字节,即string类型所占内存。
有了这个规律,我们可以通过内存平移,即:首地址+偏移量
来访问数组中的其它元素:
1. 准备工作
- 从GitHub下载可编译的objc4-818.2源码
- 准备一个类的代码,包含实例方法、类方法、属性、成员变量:
@interface GLPerson : NSObject
{
NSString *age;
}
@property (nonatomic, strong) NSString *myName;
- (void)sayHi;
+ (void)jump;
@end
2. LLDB调试查看对象和类的内存情况
对象的内存
先创建一个person
实例对象,并断点调试
// ISA_MASK = 0x00007ffffffffff8ULL
GLPerson *person = [GLPerson alloc];
person.name = @"Olive";
NSLog(@"person = %p", person);
通过lldb
打印person
对象的4段内存情况:
(lldb) x/4gx person
0x10140ede0: 0x011d80010000822d 0x0000000000000000
0x10140edf0: 0x00000001000041e0 0x0000000000000000
(lldb) po 0x011d80010000822d & 0x00007ffffffffff8ULL
GLPerson
(lldb) p/x 0x011d80010000822d & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x0000000100008228
- 用对象第一段地址
0x011d80010000822d
也就是isa和ISA_MASK
进行与操作,可以得到isa中存储的类信息$3
这里我们可以验证下$3
却是就是GLPerson的类信息:
(lldb) p/x GLPerson.class
(Class) $8 = 0x0000000100008228 GLPerson
我们继续对打印$3
也就是GLPerson类的内存情况:
(lldb) x/4gx $3
0x100008228: 0x0000000100008250 0x000000010036a140
0x100008238: 0x0000000101230500 0x000880240000000f
(lldb) po 0x0000000100008250
GLPerson
-
GLPerson
类的isa
打印仍然是GLPerson
?我们继续探究0x0000000100008250
(lldb) x/4gx 0x0000000100008250
0x100008250: 0x000000010036a0f0 0x000000010036a0f0
0x100008260: 0x0000000101231180 0x0001e03500000007
(lldb) po 0x000000010036a0f0
NSObject
(lldb) x/4gx 0x000000010036a0f0
0x10036a0f0: 0x000000010036a0f0 0x000000010036a140
0x10036a100: 0x0000000100648100 0x0003e03100000007
通过一步步打印内存,发现
0x0000000100008250
的isa
指向了NSObject
,而继续打印0x000000010036a0f0
的内存,发现其isa
仍是自己。其实在上述步骤中的
0x0000000100008228
和0x0000000100008250
都打印为GLPerson
,但他们并不是一个东西,前者为GLPerson
类,后者为其根元类。
通过上面的lldb调试分析,也就印证了一副经典的isa
走位图:
- 对象的
isa
指向类,类的isa
指向元类,元类的isa
指向根元类,根元类的isa
指向自己。
更通俗点,放到当前案例中就是:
3. 类的结构
从上面isa走位看出,一切对象最终都会指向NSObject,那么类的结构究竟是怎样的呢?我们类中定义的属性、方法又存储在哪里?
在上一节对象的本质与isa中通过clang编译.cpp文件查看对象结构时发现,最终都来自于一个NSObject_IMPL
结构体,其中的Class
是以objc_class
结构体为模板创建的。
struct NSObject_IMPL {
__unsafe_unretained Class isa;
};
typedef struct objc_class *Class;
objc_class
打开objc4-818.2源码查看objc_class
结构:
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable // 16字节
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
Class getSuperclass() const {
...
}
void setSuperclass(Class newSuperclass) {
...
}
class_rw_t *data() const {
return bits.data();
}
......省略n行代码
}
-
objc_class
其实是继承自objc_object
,在早期的版本中,isa是直接定义在objc_class
结构体中的,但现在已经被注释了,其实isa是来自父类objc_object
中:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
整体关系图:
结构分析
- class_data_bits_t
我们可以通过类的首地址
+偏移量
访问bits
,偏移量的大小为isa 8字节
+superclass 8字节
+cache 16字节
,即首地址
通过地址平移
32字节。
(lldb) x/4gx $3
0x100008228: 0x0000000100008250 0x000000010036a140
0x100008238: 0x0000000101230500 0x000880240000000f
上面例子中$3
的首地址0x100008228
平移32字节后为0x100008248
,通过类型强转得到:
// 地址平移得到class_data_bits_t
(lldb) p (class_data_bits_t *)0x100008248
(class_data_bits_t *) $4 = 0x0000000100008248
- class_rw_t
获取到class_data_bits_t
的指针地址$4
后,可以通过``中提供的data()
方法,直接读取bits中的data数据:
// 获取bits中的class_rw_t数据
(lldb) p $4->data()
(class_rw_t *) $5 = 0x00000001007060a0
// 输出class_rw_t
(lldb) p *$5
(class_rw_t) $6 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000144
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
从上面$6
指针的打印中不能明显看出些什么,我们跳转进入class_rw_t
结构体,看到里面有几个核心的方法:
通过上面几个方法的命名,可以猜测后几3个是用来获取方法列表
、属性列表
、协议列表
的,同样通过lldb验证:
(lldb) p $6.methods()
(const method_array_t) $7 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100008098
}
arrayAndFlag = 4295000216
}
}
}
(lldb) p $7.list
(const method_list_t_authed_ptr<method_list_t>) $8 = {
ptr = 0x0000000100008098
}
(lldb) p $9.ptr
(method_list_t *const) $10 = 0x0000000100008098
(lldb) p *$10
(method_list_t) $11 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 6)
}
// 打印method_list_t中方法个数
(lldb) p $11.count
(uint32_t) $12 = 4
(lldb) p $11.get(0).name()
(SEL) $12 = "sayHi"
(lldb) p $11.get(1).name()
(SEL) $13 = "name"
(lldb) p $11.get(2).name()
(SEL) $14 = ".cxx_destruct"
(lldb) p $11.get(3).name()
(SEL) $15 = "setName:"
method_list_t
继承则entsize_list_tt
,可以通过count
属性获得$10
中方法数量为4,以及通过get(n)
方法来依次打印它们。
从上面可以看到,实例方法
、属性的setter
、getter
方法、cxx_destruct
都存储在rw
的methods
当中。但是一开始我们创建的【类方法】+ (void)jump
并没有输出。
我们用类似的方法再去打印属性列表properties
:
(lldb) p $6.properties()
(const property_array_t) $16 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x00000001000081a0
}
arrayAndFlag = 4295000480
}
}
}
(lldb) p $16.list
(const RawPtr<property_list_t>) $17 = {
ptr = 0x00000001000081a0
}
(lldb) p $17.ptr
(property_list_t *const) $18 = 0x00000001000081a0
(lldb) p *$18
(property_list_t) $19 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 1)
}
(lldb) p $19.count
(uint32_t) $20 = 1
(lldb) p $19.get(0)
(property_t) $21 = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
打印得到rw
的properties
中存储了类的属性,但成员变量NSString *age;
并不在里面。那成员变量会存储在哪呢?
- class_ro_t
在class_rw_t
中还有一个class_ro_t
没有验证,我们来看一下:
(lldb) p $3.ro()
(const class_ro_t *) $22 = 0x00000001000080a8
(lldb) p *$22
(const class_ro_t) $23 = {
flags = 388
instanceStart = 8
instanceSize = 24
reserved = 0
= {
ivarLayout = 0x0000000100003f70 "\U00000002"
nonMetaclass = 0x0000000100003f70
}
name = {
std::__1::atomic<const char *> = "GLPerson" {
Value = 0x0000000100003f67 "GLPerson"
}
}
baseMethodList = 0x00000001000080f0
baseProtocols = nil
ivars = 0x0000000100008158
weakIvarLayout = 0x0000000000000000
baseProperties = 0x00000001000081a0
_swiftMetadataInitializer_NEVER_USE = {}
}
class_ro_t
中有个ivars
字段:
(lldb) p $23.ivars
(const ivar_list_t *const) $24 = 0x0000000100008158
(lldb) p *$24
(const ivar_list_t) $25 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 2)
}
(lldb) p $25.get(0)
(ivar_t) $26 = {
offset = 0x00000001000081d0
name = 0x0000000100003e90 "age"
type = 0x0000000100003f7a "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $25.get(1)
(ivar_t) $27 = {
offset = 0x00000001000081d8
name = 0x0000000100003e94 "_name"
type = 0x0000000100003f7a "@\"NSString\""
alignment_raw = 3
size = 8
}
ivar_list_t
继承自entsize_list_tt
,可以用get(n)
方法来获取列表中的值,也就是成功的读取到了age
和属性name
生成的成员变量_name
。
最后在看一下刚刚没有获取到的类方法存储在哪里。
类方法的存储
类的isa
指向元类,我们来获取元类的内存信息:
我们拿到了元类的首地址0x1000081e0
,对此来进行地址平移
读取class_data_bits_t
、rw
数据:
// 首地址 0x1000081e0 平移32位得到 0x100008200
(lldb) p (class_data_bits_t *)0x100008200
(class_data_bits_t *) $3 = 0x0000000100008200
// 读取class_rw_t
(lldb) p $3->data()
(class_rw_t *) $5 = 0x000000010073fd00
(lldb) p *$5
(class_rw_t) $6 = {
flags = 2684878849
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4302569489
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fff80170eb0
}
(lldb) p $6.methods()
(const method_array_t) $7 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100008088
}
arrayAndFlag = 4295000200
}
}
}
(lldb) p $7.list
(const method_list_t_authed_ptr<method_list_t>) $8 = {
ptr = 0x0000000100008088
}
(lldb) p $8.ptr
(method_list_t *const) $9 = 0x0000000100008088
(lldb) p *$9
(method_list_t) $10 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $10.get(0).name()
(SEL) $12 = "jump"
最终在rw
的method
中读取到了GLPerson
中的类方法+ (void)jump;
。
- 所以一个类的类方法,是已对象方法存储在其元类的
class_rw_t
中。