[OC底层]类的原理分析

350 阅读9分钟

1:首先熟悉几个LLDB的指令

  • LLDB打印isa信息:
    • x/4gx:读取内存,打印内存中存储的数据
    • p/x:以16进制形式输出
    • 0x00007ffffffffff8:是ISA_MASK的值,&该值的目的是取出NONPOINTER_ISA中的isa

1:类信息分析

编写代码, 并打断点 Screenshot 2021-10-10 at 10.47.39.png

  • 为什么用isa & mask就能得到类地址呢? 与上mask,其实是 首地址位运算,之前我们讲过,类的信息,都是存储在 isa_tshiftcls中,而shiftcls 如上图,是位于 3 字节之后,所以为了能 取出 shiftcls, 我们需要进行位移运算,而mask是系统提供的,在 _x86_64系统下的掩码,可以快速运算;

在上面的例子中, 我们打印实例变量pisa 输出的是LGPerson 类. 那么下面我们打印一下LGPerson类的isa, 看一下他的输出.

Screenshot 2021-10-10 at 11.13.29.png

- 上图LLDB制定所打印的内容:

  • p/x p 打印LGPerson的实例对象p的内存地址

  • x/4gx 0x00000001005b3870 格式化打印0x00000001005b3870地址下的连续地址空间内存储的数据,拿到了LGPerson对象首地址,也就是isa指针地址

  • p/x 0x011d800100008365 & 0x00007ffffffffff8 通过isa指针和ISA_MASK的与操作,解析了LGPerson类对象首地址

  • po 0x0000000100008360 打印这个地址数据,得到了LGPerson

  • x/4gx 0x0000000100008360通过实例对象的isa指向类对象,我拿到了类对象内存地址0x0000000100008360,格式化输出类对象的内存地址

  • p/x 0x0000000100008338 & 0x00007ffffffffff8将类对象的首地址(isa指针地址)0x0000000100008338ISA_MASK做与操作,得到了元类·的内存地址0x0000000100008338

  • 0x0000000100008360 VS 0x0000000100008338虽然地址不同,但是打印都是LGPerson,是类地址元类地址的区别

  • LGPerson: 猜想 类会和我们的对象 无限开辟 内存不止有一个类 为什么打印实例变量pisa和类的isa输出都是LGPerson.

然后我们继续验证一下类对象在内存里是不是存在多份, 打印4种方式获取类对象的地址:

Screenshot 2021-10-10 at 11.33.37.png 通过上面的打印结果,说明类对象并没有多个,而且这个类对象地址和实例对象的isa指向的地址一致。类对象的isa指向的是一个新的东西即元类。

那么LGPerson类的地址是什么呢?

这就引出了元类, 通过对象的isa,我们摸到了,通过类的isa,我们摸到了元类 Screenshot 2021-10-10 at 11.37.26.png

  • 总结一下规律:
    • 实例变量的isa指向类LGPerson
    • LGPerson类的isa指向另一个LGPerson类(这两个LGPerson类不是同一个,因为他们的地址不同,实际上第二个LGPerson类是元类)
    • 打印LGPerson类的内存地址,通过此验证得知,对象的isa指向类,类的isa指向元类

但是,在iOS中,从未有过元类这个定义;我们换另一种方式,查看一下元类是否真实存在: 将工程的可执行文件,拖入MachOView工具中,查看oc底层文件

Screenshot 2021-12-18 at 11.54.25 PM.png

我们在符号表中搜索class关键字,发现了_OBJC_METACLASS_$_JSPerson,说明编译器确实在编译期生成了元类对象。

现在我们都知道编译器会帮我们生成元类, 那么接下来 我们看一下元类对象是否也有isa指针? 接下来,我们用LLDB继续探索。

1:

x/4gx 0x00000001000081f8

0x1000081f8: 0x00000001000081d0 0x0000000202874d08

0x100008208: 0x0000000197304e60 0x0000802900000000

(lldb) po 0x00000001000081d0 & 0x00007ffffffffff8

LGPerson

2:

x/4gx LGPerson.class

0x1000081f8: 0x00000001000081d0 0x0000000202874d08

0x100008208: 0x0000000197304e60 0x0000802900000000

(lldb) p/x 0x00000001000081d0  & 0x00007ffffffffff8

(long) $8 = 0x00000001000081d0

(lldb) po 0x00000001000081d0

LGPerson

3:

x/4gx 0x00000001000081d0

0x1000081d0: 0x0000000202874ce0 0x0000000202874ce0

0x1000081e0: 0x0003000100515e50 0x0002e03500000000

(lldb) p/x 0x0000000202874ce0 & 0x00007ffffffffff8

(long) $10 = 0x0000000202874ce0

(lldb) po  0x0000000202874ce0

NSObject

4:
(lldb) p/x NSObject.class

(Class) $12 = 0x0000000202874d08 NSObject

(lldb) x/4gx 0x0000000202874ce0

0x202874ce0: 0x0000000202874ce0 0x0000000202874d08

0x202874cf0: 0x00070001005ba780 0x0001e03400000000

(lldb) p/x 0x0000000202874ce0 & 0x00007ffffffffff8

(long) $13 = 0x0000000202874ce0

(lldb) po 0x0000000202874ce0

NSObject

通过4次打印, 前两次为LGPerson 后两次为NSObject. 通过对LGPerson的层层探索, 最终我们找到了NSObject. 然后, 我们在打印一下LGPerson类在内存中的地址 Screenshot 2021-10-10 at 11.20.42.png

isa走向总结:

通过上述过程,来绘制一个简单的流程图: Screenshot 2021-12-20 at 10.04.18 PM.png

通过对象的isa, 我们探索到了类. 再通过类的isa, 探索到元类, 继续探索到根元类,也就是NSObject, 接下来就形成了闭环,回归NSObject.

通过这些探索,我们统一思想,iOS里,任何对象,他的元类的父类,都是根元类,也就是NSObject,所以NSObject是一切类的基础。

类继承链:

iOS 中,类是由继承关系的。但是最终的父类是谁,父类又源至谁,我们都比较模糊,接下来,我们就彻底理清一下这些继承关系. 基本概念

  • 根类:在OC中几乎所有的类都继承自NSObject类,NSObject类就是根类,根类的父类为nil
  • 元类:在我们平时开发中会用到类方法和实例方法,但是在底层的实现中并没有这种区分,实际上都是通过实例方法去查找的,底层为了区分这两种方法,引入元类的概念,然后将实例方法存储在类中,类方法存储在元类中。类的isa指向元类。
  • 根元类:即为根类NSObjectisa指向的类
关系图:

isa流程图.png 总结下上图:

  • isa的指向:

    • 实例变量的isa指向对应的类
    • 类的isa指向对应的元类
    • 元类的isa指向根元类
    • 根元类的isa指向自身
  • 类的继承:

    • 类的superclass指向父类
    • 父类的superclass指向根类
    • 根类的superclass指向nil
  • 元类的继承:

    • 元类的superclass指向对应类的父类的元类
    • 父类的元类的superclass指向根元类
    • 根元类的superclass指向根类
    • 根类的superclass指向nil

补充:结构体内存平移取值

举例:
struct EBStruct {
    int a;     // 偏移:0, 占4字节
    char b;    // 偏移:4, 占1字节
    int c;     // 偏移:8, 占4字节
    long d;    // 偏移:16,占8字节(成员变量的开始位置为当前成员变量所占内存的整数倍)
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct EBStruct s;
        s.a = 9;
        s.b = 'n';
        s.c = 8;
        s.d = 7;
    }
    return 0;
}

取出某个成员变量的值:

  • 先确定结构体变量地址
  • 确定成员变量的偏移位置
  • 计算成员变量的内存地址:结构体变量地址+该变量偏移位置
  • 将计算得到的地址转为该变量对应的类型的指针类型(例如aint类型,则将计算的地址转为int *类型,这样转的目的是取出int类型的值) 下面来实际操作一下:

Screenshot 2021-12-26 at 10.31.19 PM.png

类的存储:

  • 通过前面的分析,我们对类之间的关系和类的结构有了一个大致的了解,但是我们还不是很清楚类在内存中具体是如何存储的?

    • 属性存储在哪里?
    • 成员变量存储在哪里?
    • 方法存储在哪里?
  • 下面我们通过LLDB结合源码的方式分析下类在内存中的存储

类的bits分析

下载好objc的源码, 并定义EBPerson类:

#import <Foundation/Foundation.h>

@interface EBPerson : NSObject {
    NSString *subject;
}

@property (nonatomic, copy) NSString *ebName;
@property (nonatomic, strong) NSString *nickName;
- (void)say;
+ (void)play;

@end

@implementation EBPerson

- (**instancetype**)init{

 if (self = [super init]) {
   self.name = @"Er Bao";
}
return self;
}

- (void)say {}
+ (void)play {};

@end

首先从源码中搜索到类的定义:

struct objc_class : objc_object {
    Class ISA;                 // isa指针

    Class superclass;          // 存储当前父类

    cache_t cache;             // 用于缓存指针和 vtable,加速方法的调用

    class_data_bits_t bits;    
    
    // 相当于 class_rw_t 指针加上 rr/alloc 的标志,`bits` 用于存储类的方法、属性、遵循的协议等信息的地方; `class_data_bits_t 内部,包装有写入bits的方法 data(),最终返回class_rw_t的结构体`

    ........
}

class_rw_t* data() const {
     return (class_rw_t *)(bits & FAST_DATA_MASK);
}

我们之前可以获取LGPerson类的地址,类的地址,其实就是当前内存结构里的首地址,我们可以按照内存平移的步骤,获取类里的bits; 首先看下cache_t的定义:

struct cache_t {

private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
        explicit_atomic<mask_t>    _maybeMask;  // 4字节(uint32_t)
#if __LP64__
            uint16_t                   _flags;  // 2字节
#endif
            uint16_t                   _occupied;  // 2字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
        // 静态变量和方法是不占用内存的
    };

cache_t的定义可以看出:

  • 第一个成员变量_bucketsAndMaybeMaskuintptr_t类型,实际为unsigned long类型,占8字节

  • 第二个成员变量为联合体(联合体成员共享同一块内存)

    • 联合体的第一个成员为结构体,最大占8字节
    • 联合体的第二个成员为指针类型,占8字节
    • 所以,联合体占内存大小即为最大成员占内存大小,即为8字节
  • 所以,cache_t占内存为16字节 bits数据的获取:

  • 通过前面介绍的结构体内存平移取值的方法可以获取bits的值

  • 在获取bits的值之前需要先确定bits的偏移量

  • 前两个成员变量ISAsuperclass都是Class类型,而Classstructobjc_class *类型,占8字节

  • 第三个成员变量cachecache_t类型,占16字节

  • 所以,成员变量bits的偏移地址为32字节(可以通过结构体内存地址+0x20计算得到)

下面来看一下class_data_bits_t的定义:

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_flags above.
    uintptr_t bits;
    // ...
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    // ...
};
  • class_data_bits_t的定义可以看出,可以通过data()获取数据,结果为class_rw_t类型 下面看一下class_rw_t的定义:
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif

    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;

	// ...

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

    // ...

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

从上面class_rw_t的定义可以看出有几个重要的方法:

  • methods()
  • properties()
  • ro()

Reference

juejin.cn/post/697555…

juejin.cn/post/697583…

juejin.cn/post/697809…