OC 类内存布局之 bits

635 阅读5分钟

我们每天都在用类创建对象, 那么类的内存结构到底是怎样的, 今天我们就来看看, 探索一下. 今天研究的主要是 属性, 成员, 方法 在类中的内存布局; 研究的手段主要是结合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 的相关的定义, 和 bitsdata() 方法返回值类型 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 , 通过 bitsdata() 方法获取到 class_rw_t 类型的返回值, 这一步后来我发现用类的 data() 方法也是可以得到的, 实际还是调用的 class_rw_tdata(), 方法后面的过程中会演示出来; 然后根据分别调用 class_rw_tproperties() 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;
}

查看属性列表

  1. 打印出类的地址;
  2. 计算出 bits 的地址, 并加上类型强转;
  3. bits 调用 data() 方法, 找到 class_rw_t * 类型的返回值;
  4. 调用 class_rw_tproperties() 方法, 返回的是属性数组的结构体;
  5. 沿着 list -> ptr 最终找到 property_list_t 属性列表结构体, 这里显示了属性数量 count = 2;
  6. 通过 property_list_tget(uint32_t i)方法, 按下标分别取出对应属性; image.png

两种方法得到 class_rw_t * 类型的返回值. image.png

查看对象方法列表

清空一下调试区, 我们继续调试, 接着 $13 开始.

  1. 调用 class_rw_tmethods() 方法, 返回的是方法数组的结构体;
  2. 沿着 list -> ptr 最终找到 method_list_t 属性列表结构体, 这里显示了属性数量 count = 2; image.png 因为方法比较多, 所以再次清空调试区, 接着 $17 开始调试;
  3. 通过 method_list_tget(uint32_t i)方法, 按下标分别取出对应的方法;
  4. 通过 method_tbig() 方法查看方法的详细信息, 用同样的方法查看所有的方法.
  • sayNB 方法 image.png
  • gf_name 的 get 和 set 方法 image.png image.png
  • c++ 的析构方法 image.png
  • age 的 get 和 set 方法 image.png image.png
  • 报越界错误 image.png

小结

从以上的调试中, 我们找到了类的属性和对象方法的内存布局; 但是我们从属性列表中并没有发现成员变量 car_name, 说明成员变量并不在属性列表中; 方法列表中也并没有发现类方法 say666, 说明类方法并没有在方法列表中, 另外还有属性的 gettersetter, 这也证明了我们用了这么久的 @property 声明属性这个特性.

查看成员变量列表

大家应该都用过 runtimeclass_copyIvarList 函数获取过成员列表, 关键词就是 ivar, 正好在 class_ro_t 中就有, class_rw_tro() 方法返回值就是这个类型, 我们可以试试看, 万一对了呢😄😄😄

我们接着 class_rw_t 往下摸索, 先把 class_rw_t 重新打印出来, 也就是 $6;

注意: 我这里因为重新运行了项目, 所以跟属性列表中的 $6 是对不上的.

  1. 调用 class_rw_tro() 函数, 返回的是 class_ro_t * 类型的返回值;
  2. 查看 $37;
  3. 查看 class_ro_tivars 成员;
  4. 查看 $39, 到这一步我们已经找到 ivar_list_t, 显示有3个成员 count = 3; image.png
  5. 调用 ivar_list_tget() 方法查看各个成员变量, 有 car_name _age _gf_name image.png

以上就是成员变量列表的内存布局, 除了我们直接声明的成员变量, 还有系统为 @property 声明的属性默认生成的带下划线的成员变量.

查看类方法列表

我们在类的方法列表中并没有找到 say666 这个类方法, 众所周知类方法是存在于 元类 中, 元类也是类, 所以结构一样, 类的 isa 指向的就是元类, 所以比查看对象方法多一步, 就是要通过类的 isa 找到元类里去, 查看元类的方法列表, 那么接下来我们来验证一下.

我们接着 $1 类的地址往下摸索, 到元类中去找.

  1. 查看类的内存, 主要是找到 isa 的值;
  2. 计算元类的首地址: isa & ISA_MASK;
  3. 计算元类 bits 的首地址;
  4. 顺藤摸瓜按照查看对象方列表的操作找到方法列表, 并按下标依次查看. 这个步骤大家应该就熟悉了, 就不再一一写出来了, 最后我们找到了 say666 这个类方法. image.png

总结

类的 isasuperclass 大家都比较熟悉就不多说了, cache 这里先不探索, 主要 bits;

属性, 成员变量, 对象方法 都存在于类的 bits, 类方法存在 元类 中; 在底层本没有对象方法类方法 之分, 只是人为在上层给方法加以区分;

相关面试题

对象和类底层结构的区别

  1. 区别是对象在底层的模板结构是 struct objc_object, 类的底层模板是 struct objc_class.
  2. 相同的是都有 isa, 所以 NSObject 对象创建出来是占有 16 个字节内存, 实际需要的内存只有 8字节, 即 isa的大小.
  3. 对象底层充满了属性和成员变量, 所以对象的大小取决于属性和成员的数量.

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; 
}