作者:穿靴子的树
链接:https://juejin.cn/post/6912359669704949767

objc 源码下载地址

首先到苹果objc源码官网下载一个最新的包,这里下载的是 objc4-781.tar

Class 和实例的数据结构

看一个例子

// ---------------- Animal ---------------- 

@interface Animal : NSObject {
@public
    int _age;
}

- (void)run; // 实例方法
+ (void)animalClassRun; // 类方法

@end

@implementation Animal

- (void)run {
    // ...
}

+ (void)animalClassRun {
    // ...
}

@end

// ---------------- Cat ---------------- 

@interface Cat : Animal {
@public
    int _legs;
}
- (void)jump; // 实例方法

@end

@implementation Cat

- (void)jump {
    // ...
}

@end

继承关系是: Cat: Animal: NSObject

测试一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
        cat->_age = 6;
        cat->_legs = 4;
        [cat run];
        NSLog(@"cat %p", cat);
    }
}

根据输出地址查看cat对象里面的内存数据(x/2xg 指令是查看前面16个字节的内存数据):

(lldb) x/2xg 0x1004aff60
0x1004aff60: 0x001d800100008299 0x0000000400000006

可以看到对象里面有具体的实例变量的值 0x0000000400000006 。0x00000004 和 0x00000006。那么0x001d800100008299是什么呢?它其实是经过一些运算得到的 isa 指针地址

查看objc源码里面的实例对象的结构体 objc_object 。它是个这样的东西:

顺便找到 Class 的内部代码, 是个这样的东西:

好吧,它们内部其实都是结构体指针(可以看到 struct objc_class : objc_object,所以类其实也是对象)。

存储的数据

实例,类,元类的关系:

实例 (通过isa指针 --->) 类 (通过isa指针 --->) 元类

对象的存储
这里有一个iOS交流圈:891 488 181 不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

当我们初始化一个 Cat Class 的实例对象的时候,对象里面其实存储了一个指向 Cat Classisa 指针,之后会存储实例变量的值。也就是这样的:

isa   // 指针地址,指向Cat类
6     // _age 变量
4     // _legs 变量

Class 的存储

Class 里面保存了一个指向这个class 的元类 MetaClassisa 指针, 一个指向父类的 Class superclass 指针。当然里面还存储了属性列表,实例方法列表,方法缓存列表,协议列表,实例变量列表等信息

在源码里面找一下,大概是这样一个结构:

objc_class

struct objc_class : objc_object {
    // Class ISA;      // 继承来的 isa 指针
    Class superclass;  // 父类指针
    cache_t cache;             // 方法缓存 formerly cache pointer and vtable
    class_data_bits_t bits;    // 获取具体的类信息 class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }
    // .... more
    // .... more
    // .... more

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;

private:
    using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t *, class_rw_ext_t *>; // 这个具体是什么不理它,不过应该是从这里面获取信息

    // .... more
    // .... more
    // .... more

class_rw_ext_t

struct class_rw_ext_t {
    const class_ro_t *ro;
    method_array_t methods;       // 方法列表
    property_array_t properties;  // 属性列表
    protocol_array_t protocols;   // 协议列表
    char *demangledName;
    uint32_t version;
};

class_ro_t

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;

    const char * name; // 类的名字
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;  // 成员变量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    // .... more
    // .... more
    // .... more

元类 MetaClass 的存储

由于元类和类的结构体是一样的都是 Class 类型,只不过里面没有属性等之类的信息,然后里面存储的是类方法列表。 所以它里面大致也就是这样:

    Class isa;         // isa 指针
    Class superclass;  // 父类指针
    // .... more
    // .... more
    // .... more

isa 和 superclass 指针是来干什么的?

画了一张粗略的关系图:

其中 cat 是实例,Cat是类, MetaCat 是对应的元类。虚线是对应的 isa 指针的指向,实线是对应的 superclass 指针的指向。

由于实例方法存储在类中,类方法存储在对应的元类中。所以 isasuperclass 指针的一大作用是方法的查找。比如

[cat jump];

那么 cat实例 会通过它的 isa 指针找到 类Cat,然后在实例方法列表里面查看 jump方法的实现(当然它会先在方法缓存里面先看看是否存在)。

如果是这样的:

[cat run];

cat实例 会通过它的 isa 指针找到 类Cat,然后在方法列表里面查看 run方法的实现,如果找不到,那么就通过 superclass 指针到父类的方法列表里面找,如果还没找到就一直找到 NSObject 里面。要是 NSObject 里面都没找到,那么就报错了(当然里面还涉及了方法转发相关)。 顺序是这样的:

Cat -> Animal -> NSObject

当然如果是下面这样的类方法调用:

[Cat animalClassRun];

那么它会到 MetaCat 元类里面查找方法的实现,查找的顺序是这样的:

MetaCat -> MetaAnimal -> MetaNSObject -> NSObject

总之方法查找就是先通过自己的 isa 指针查找方法,如果没有找到,那么再通过 superclass 指针一直到 NSObject 查找。

验证一下 isa 和 superclass 指针内存的指向

添加一个测试:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Cat *cat = [[Cat alloc] init];
        cat->_age = 6;
        cat->_legs = 4;
        [cat run];
        NSLog(@"Cat instance %p", cat);

        // 测试一下不同方法的输出
        Class catClass0 = [cat class];
        Class catClass1 = [Cat class];
        Class catClass2 = object_getClass(cat);
        Class catClass3 = objc_getClass("Cat");

        Class catMeta = object_getClass(catClass3);

        NSLog(@"Cat class %p, %p, %p, %p", catClass0, catClass1, catClass2, catClass3);
        NSLog(@"Cat Meta %p", catMeta);

        Class animalClass = [Animal class];
        Class animalMeta = object_getClass(animalClass);

        NSLog(@"Animal class %p", animalClass);
        NSLog(@"Animal Meta %p", animalMeta);

        Class objClass = [NSObject class];
        Class objMeta = object_getClass(objClass);

        NSLog(@"NSObject Class %p", objClass);
        NSLog(@"NSObject Meta %p", objMeta);
    }

    return 0;
}

输出:

2020-12-31 17:22:25.113922+0800 OCZ[50658:3217214] Cat instance 0x10065eb60
2020-12-31 17:22:25.114216+0800 OCZ[50658:3217214] Cat class 0x100008298, 0x100008298, 0x100008298, 0x100008298
2020-12-31 17:22:25.114266+0800 OCZ[50658:3217214] Cat Meta 0x100008270
2020-12-31 17:22:25.114318+0800 OCZ[50658:3217214] Animal class 0x100008248
2020-12-31 17:22:25.114350+0800 OCZ[50658:3217214] Animal Meta 0x100008220
2020-12-31 17:22:25.114388+0800 OCZ[50658:3217214] NSObject Class 0x7fff8e454118
2020-12-31 17:22:25.114418+0800 OCZ[50658:3217214] NSObject Meta 0x7fff8e4540f0

其中:

0x10065eb60 cat 实例内存地址

0x100008298 Cat 类地址

0x100008270 MetaCat 元类地址

0x100008248 Animal 类地址

0x100008220 MetaAnimal 元类地址

0x7fff8e454118 NSObject 类地址

0x7fff8e4540f0 MetaNSObject 元类地址

由于只需要查看内存里面的 isa 和 superclass 指针内存的地址,而且从之前的内存布局也知道它俩就在内存的开始位置,所以也就只需要查看前面16个字节的数据就好了(每个指针占用8个字节),使用指令 x/2xg 查看即可。

下面是具体的调试信息

(lldb) x/2xg 0x10065eb60
0x10065eb60: 0x001d800100008299 0x0000000400000006
(lldb) p/x 0x001d800100008299 & 0x00007ffffffffff8ULL
(unsigned long long) $0 = 0x0000000100008298
(lldb) x/4xg 0x100008298
0x100008298: 0x0000000100008270 0x0000000100008248
0x1000082a8: 0x000000010065c850 0x0002801800000003
(lldb) x/2xg 0x100008270
0x100008270: 0x00007fff8e4540f0 0x0000000100008220
(lldb) x/2xg 0x100008248
0x100008248: 0x0000000100008220 0x00007fff8e454118
(lldb) x/2xg 0x7fff8e454118
0x7fff8e454118: 0x00007fff8e4540f0 0x0000000000000000
(lldb) x/2xg 0x7fff8e4540f0
0x7fff8e4540f0: 0x00007fff8e4540f0 0x00007fff8e454118
(lldb) 

通过调试 isa 和 superclass 指针的指向的确是如前面所说那样。

其中为什么执行这个操作 0x001d800100008299 & 0x00007ffffffffff8ULL 是因为实例的isa指针指向需要执行一个 & 操作 取 ISA_MASK 的值 0x00007ffffffffff8ULL(调试的时候是在Mac命令行项目)。这是具体的源码信息:

如果是在手机上面调试的的,则 ISA_MASK 的值是 __arm64__# define ISA_MASK 0x0000000ffffffff8ULL

为什么要这样设计

由于在运行的时候可能会创建大量的类的实例,每个实例里面的变量的值都可以不一样,所以实例只需要存储有具体的实例变量的值,和一个方法查找的isa指针就好了。

而类和元类在整个代码运行的时候只要有一份就好了。毕竟不管是实例方法还是类方法它们具体的执行都是 objc_msgSend(void)这个消息发送方法。

实例存储: isa指针, 实例变量列表的具体的值

类存储: isa指针, superclass指针, 属性列表,实例方法列表,实例变量列表,协议列表等

元类存储: isa指针, superclass指针, 类方法列表等

补充

object_getClass 的实现:

如源码所示,object_getClass 里面是通过 isa 指针的指向实现的。所以我们可以知道

object_getClass(实例对象) 得到的是对应的类

object_getClass(类) 得到的是对应的元类

objc_getClass 的实现:

如源码所示,objc_getClass 需要传入的是类的字符串名字,如果名字正确就得到对应的类。

文章到这里就结束了,你也可以私信我及时获取最新资料以及面试相关资料。如果你有什么意见和建议欢迎给我留言。