我们每天都在用类创建对象, 那么类的内存结构到底是怎样的, 今天我们就来看看, 探索一下. 今天研究的主要是 属性, 成员, 方法 在类中的内存布局; 研究的手段主要是结合objc 的源码 和 lldb 调试查看类的结构, 只有在源码里才有那些底层的结构体类型, 否则调试的时候底层那些类型是找不到的.
调试准备工作
准备好 objc 的源码工程, 这个工程可以到苹果官网去下载.
通过查看 objc 的源码, 可以发现类的底层结构 struct objc_class 如下, 继承自 struct objc_object, 我们的把源码整合一下, 实际就相当于有 4 个成员, 这里我们只看成员, 不管其他的.
objc 的源码版本: 818.2
struct objc_class {
isa_t isa; // 8字节
Class superclass; // 8字节
cache_t cache; // 16字节
class_data_bits_t bits; // 8字节
// 方法定义太多, 暂时省略...
class_rw_t *data() const {
return bits.data();
}
}
因为我们主要研究的是bits, 所以除了类的定义, 还有 class_data_bits_t 的相关的定义, 和 bits 的 data() 方法返回值类型 class_rw_t 相关的定义, 这里我们也是仅展示一部分我们需要研究的内容. 完整内容有兴趣的可以去看 objc 源码.
class_data_bits_t 定义
struct class_data_bits_t {
uintptr_t bits;
// 重要方法
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}
class_rw_t 定义中的部分方法 properties() methods() ro(), 这些方法的实现我们不需要关注, 到调试的时候就知道了.
struct class_rw_t {
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 *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->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 *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
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_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
}
首先说明一下调试的大概过程, 找到 bits , 通过 bits 的 data() 方法获取到 class_rw_t 类型的返回值, 这一步后来我发现用类的 data() 方法也是可以得到的, 实际还是调用的 class_rw_t 的 data(), 方法后面的过程中会演示出来; 然后根据分别调用 class_rw_t 的 properties() methods(), 一步一步找到我们定义的属性;
说明一下调试的时候是如何定位到 bits 的, bits 的首地址是类的首地址加上 32字节(0x20)的偏移量, 因为在它的前边有 isa superclass cache bits, 共 32 字节, 不会计算结构体大小的朋友自己百度补一下😄.
objc 的源码工程是一个 macOS 工程, 调试的时候需要创建一个 macOS 平台项目, 我这里创建一个 Command Line Tool target, 然后搞一个 Person 类, 声明两个属性, 一个成员变量, 一个对象方法, 一个类方法, 方法实现就不写出来了; 在 mian 函数中创建一个对象, 打一个断点, 然后开始运行项目调试.
@interface Person : NSObject {
NSString *car_name;
}
@property(nonatomic, copy) NSString *gf_name;
@property(nonatomic, assign) int age;
- (void)sayNB;
+ (void)say666;
@end
main 函数
int main(int argc, const char * argv[]) {
@autoreleasepool {
// class_data_bits_t
Person *p = [Person alloc];
NSLog(@"%@",p);
}
return 0;
}
查看属性列表
- 打印出类的地址;
- 计算出
bits的地址, 并加上类型强转; bits调用data()方法, 找到class_rw_t *类型的返回值;- 调用
class_rw_t的properties()方法, 返回的是属性数组的结构体; - 沿着
list->ptr最终找到property_list_t属性列表结构体, 这里显示了属性数量count = 2; - 通过
property_list_t的get(uint32_t i)方法, 按下标分别取出对应属性;
两种方法得到
class_rw_t *类型的返回值.
查看对象方法列表
清空一下调试区, 我们继续调试, 接着 $13 开始.
- 调用
class_rw_t的methods()方法, 返回的是方法数组的结构体; - 沿着
list->ptr最终找到method_list_t属性列表结构体, 这里显示了属性数量count = 2;因为方法比较多, 所以再次清空调试区, 接着
$17开始调试; - 通过
method_list_t的get(uint32_t i)方法, 按下标分别取出对应的方法; - 通过
method_t的big()方法查看方法的详细信息, 用同样的方法查看所有的方法.
- sayNB 方法
- gf_name 的 get 和 set 方法
- c++ 的析构方法
- age 的 get 和 set 方法
- 报越界错误
小结
从以上的调试中, 我们找到了类的属性和对象方法的内存布局; 但是我们从属性列表中并没有发现成员变量 car_name, 说明成员变量并不在属性列表中; 方法列表中也并没有发现类方法 say666, 说明类方法并没有在方法列表中, 另外还有属性的 getter 和 setter, 这也证明了我们用了这么久的 @property 声明属性这个特性.
查看成员变量列表
大家应该都用过 runtime 的 class_copyIvarList 函数获取过成员列表, 关键词就是 ivar, 正好在 class_ro_t 中就有, class_rw_t 的 ro() 方法返回值就是这个类型, 我们可以试试看, 万一对了呢😄😄😄
我们接着 class_rw_t 往下摸索, 先把 class_rw_t 重新打印出来, 也就是 $6;
注意: 我这里因为重新运行了项目, 所以跟属性列表中的$6是对不上的.
- 调用
class_rw_t的ro()函数, 返回的是class_ro_t *类型的返回值; - 查看
$37; - 查看
class_ro_t的ivars成员; - 查看
$39, 到这一步我们已经找到ivar_list_t, 显示有3个成员count = 3; - 调用
ivar_list_t的get()方法查看各个成员变量, 有car_name_age_gf_name
以上就是成员变量列表的内存布局, 除了我们直接声明的成员变量, 还有系统为 @property 声明的属性默认生成的带下划线的成员变量.
查看类方法列表
我们在类的方法列表中并没有找到 say666 这个类方法, 众所周知类方法是存在于 元类 中, 元类也是类, 所以结构一样, 类的 isa 指向的就是元类, 所以比查看对象方法多一步, 就是要通过类的 isa 找到元类里去, 查看元类的方法列表, 那么接下来我们来验证一下.
我们接着 $1 类的地址往下摸索, 到元类中去找.
- 查看类的内存, 主要是找到
isa的值; - 计算元类的首地址:
isa&ISA_MASK; - 计算元类
bits的首地址; - 顺藤摸瓜按照查看对象方列表的操作找到方法列表, 并按下标依次查看. 这个步骤大家应该就熟悉了, 就不再一一写出来了, 最后我们找到了
say666这个类方法.
总结
类的 isa 和 superclass 大家都比较熟悉就不多说了, cache 这里先不探索, 主要 bits;
属性, 成员变量, 对象方法 都存在于类的 bits, 类方法存在 元类 中; 在底层本没有对象方法 和 类方法 之分, 只是人为在上层给方法加以区分;
相关面试题
对象和类底层结构的区别
- 区别是对象在底层的模板结构是
struct objc_object, 类的底层模板是struct objc_class. - 相同的是都有
isa, 所以NSObject对象创建出来是占有 16 个字节内存, 实际需要的内存只有 8字节, 即isa的大小. - 对象底层充满了属性和成员变量, 所以对象的大小取决于属性和成员的数量.
objc_object 和 对象的关系
objc_object 是对象底层的模板.
属性和成员变量的区别
@interface Person : NSObject {
NSString *car_name; // 成员变量
}
@property(nonatomic, copy) NSString *gf_name; // 属性
@property(nonatomic, assign) int age; // 属性
@end
成员变量是写在类的声明或者扩展的大括号里的变量, 没有 getter 和 setter 方法;
用 @property 声明的属性, 系统会自动声明一个带下划线成员变量 和 getter / setter 方法, 用 clang 编译成 C++ 以后, 我们可以看出这个结论;
属性 = 带下划线成员变量 + getter + setter
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *car_name;
int _age;
NSString * _Nonnull _gf_name;
};
static NSString * _Nonnull _I_Person_gf_name(Person * self, SEL _cmd) {
return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_gf_name));
}
static void _I_Person_setGf_name_(Person * self, SEL _cmd, NSString * _Nonnull gf_name) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _gf_name), (id)gf_name, 0, 1);
}
static int _I_Person_age(Person * self, SEL _cmd) {
return (*(int *)((char *)self + OBJC_IVAR_$_Person$_age));
}
static void _I_Person_setAge_(Person * self, SEL _cmd, int age) {
(*(int *)((char *)self + OBJC_IVAR_$_Person$_age)) = age;
}