iOS类加载流程(二):类的静态初始化

iOS类加载流程(一):类加载流程的触发
中已经知道两个关键函数 map_images()load_images() 的触发逻辑了。但是现在直接看 map_images() 会一脸懵逼。

这里直接看老司机们都比较熟悉的 realizeClassWithoutSwift 函数。我们只需要知道这个函数是 objc 进行类的懒加载和非懒加载必须调用的方法,也是类初始化中的关键函数。理解了这个函数再去看主流程会比较清晰;

本文也不是对 realizeClassWithoutSwift 函数源码进行解读,源码的解读会放到下一部篇文章。本文更多的是通过这个函数来理清楚阅读 objc 源码的思路。另外, objc 本质上是 C/C++ 语言的封装,相比于源码,更重要的是要理解 objc 如何与编译器、静动态链接器配合,完成 OC 代码到机器指令的过程。

1. 引子

realizeClassWithoutSwift 是执行类的初始化的关键方法,代码也比较多,这里拆分来看。

第一步是获取静态数据,关键代码如下:

static Class realizeClassWithoutSwift(Class cls) {
    // 变量声明
    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    // 是否已经被map
    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));

    // 静态数据初始化
    // 因为cls是从mach-O获取到的,所以此时data()方法获取到的是静态数据,所以类型是class_ro_t而不是class_rw_t
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
      // Future Class相关,暂时省略
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        //rw因为8字节对齐,所以其后三位必定是空闲的
        // 后三位有对应的set和get方法来进行标志位的获取和设置
        cls->setData(rw);
    }
    // 补充标志位
    isMeta = ro->flags & RO_META;
    rw->version = isMeta ? 7 : 0;  // old runtime went up to 6
}

上述代码做了几件事:

  1. 变量声明和一些条件判断,没什么好说的;
  2. 为 rw 分配内存并且将 ro 赋值给 rw;
  3. 设置了一些标志位;

这里需要提一下这段代码:

// classref_t is unremapped class_t*
typedef struct classref * classref_t;

根据注释可知道 struct classref 表示没有被 map 的 class_t*。而 class_t* 就是运行时的类结构体。但是,怎么证明或者体现呢?这里可以搜一下 objc 中 classref_t 的使用,基本上会有这样一段代码:

classref

上图可以看到 classref 被强制转化成了 Class,其他几处使用到 classref_t 的地方也都是这样,总结下来两个共同点:

  1. classref_t 指向的都是从 mach-O 文件中的数据(类的静态数据);
  2. 都被强制转化成了 Class,也就是 class_t *

由此,可以知道 classref_t 就是一个只有声明没有实际结构的结构体(有兴趣可以使用 C 代码坐下实践),类似于 OC 中只有 @Interface 却没有 @implementation 的类。

继续看 realizeClassWithoutSwift 中的代码,上述代码关键在于第二步,筛选出关键代码也就几行:

rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);

这里有几个疑问需要解开:

  1. ro 和 rw 是什么?
  2. setData() 做了什么?

关于第一点,先直接说结论:

  1. ro 就是 class_ro_t 结构体,表示编译时期生成的类的静态结构,直接记录在 mach-O 文件类,被 __objc_classlist__objc_nlclslist 这两个 section 记录并映射。而 ro 的生成过程是在静态编译链接时期,可以通过 OC 转换后的 C/C++ 代码来看到表示数据的静态时期的结构体;
  2. rw 就是 class_rw_t 结构体,表示运行时类的实际结构,是 objc 动态性的体现。rw 在被 ro 初始化之后还会根据 objc 的动态性,在运行时为类添加其他属性,包括但不限于方法、属性等;

那么,这个结论是怎么来的呢?

2. 第一次尝试

OC 本质是对 C/C++ 的封装,而程序运行时,本质上都是机器指令 + 数据的存、取、处理,而数据在 C/C++ 中又是通过结构体/类来定义的。所以,这里可以尝试研究下 C/C++ 源码中类对应的结构体是怎样的。

首先来看看我们常见的 .m 文件编译之后产生的 C++ 文件。

测试代码如下,为了方便查看,全都写在了 .m 文件下:

#import <Foundation/Foundation.h>

// 父类XKPerson
@interface XKPerson : NSObject
@property (copy, nonatomic) NSString *name;
@end

@implementation XKPerson
@end

// 子类XKStudent
@interface XKStudent : XKPerson
@property (copy, nonatomic) NSString *studioName;
- (void)learn;
@end

@implementation XKStudent
- (void)learn {
    NSLog(@"%@ is learning at %@",self.name, self.studioName);
}
@end

int main(int argc, char * argv[]) {
    XKStudent *student = [XKStudent new];
    student.name = @"Jack";
    student.studioName = @"Affiliated Middle School of Tsinghua";
    [student learn];
    return 0;
}

编译 .m 文件:

 clang -rewrite-objc main.m -o main.cpp          

main 函数中代码很多,为了寻找 OC 中类的本质,思路是先来看看 XKStudent 到底是个啥。XKStudent 初始化代码简化之后如下:

XKStudent *student = (objc_msgSend)(objc_getClass("XKStudent"), sel_registerName("new"));

既然 OC 背后是 C/C++,那么 XKStudent 的定义又是什么呢?如下:

typedef struct objc_object XKStudent;

objc_object 就是我们最常见的类的定义:

typedef struct objc_class *Class;
struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};
typedef struct objc_object *id;
typedef struct objc_selector *SEL;

这里自己的理解是:使用多态的形式来表示实例变量 student,类似于可以使用 NSObject * 或者 id 来表示 OC 中任何一个实例变量。

但是,这里有两个疑问:

  1. 源码中有使用到 struct XKStudent,而 XKStudent 是别名,已经包含了 struct,所以不需要带 struct,那这个 struct XKStudent 必定存在,在哪被声明的呢?结构体又是怎样?
  2. 除了 isa 指针外,每个实例变量都有一份自己的成员变量,而 XKStudent 是别名,本质是 struct objc_object,而 struct objc_object 内部只有一个 isa 指针,如何表示类的结构呢?

因此,必定还存在一个 struct XKStudent 的结构体定义。

可以先不管 OC 中方法寻找流程,但是最少需要这么一个结构体来让编译器或者 ide 知道这个类有哪些成员属性,进而进行内存分配或者是代码提示。

XKStudent 在源码中有三种用法:

  1. XKStudent;
  2. struct XKStudent;
  3. struct XKStudent_IMPL;

XKStudent 就是上文的 objc_object 进行了一个 typedef 操作,是多态的利用。在使用 typedef struct XKStudent XKStudent; 之后,别名是 XKStudent,已经包含了 struct,不需要再用 struct 修饰了;

而第二种用法中, struct 是 C 语言中的结构体,仍然使用 struct 修饰表明这是一个实际存在的结构体。所以 struct XKStudent 这种使用形式下,必定存在一个 struct XKStudent {xxx} 的定义的(否则第一种用法都不能使用 typedef 来定义别名)。

最简单的例子,比如:

结构体实例

但是整个编译之后的源码找不到 struct XKStudent {} 这种形式的代码,所以,猜测可能是在其他地方定义的,只不过编译之后的源码没暴露出来。这里猜测的依据是两断代码,第一段:

XKStudent_IMPL

这里 XKStudent_IMPL 这个结构体只在这一个地方被使用到,而且只是用来计算 size,所以,大概率这个结构体实际上并没有被使用到,完成了 size 的计算使命之后就不再使用了~~~

还有就是 struct XKStudent 相关的代码:

XKStudent

struct XKStudent 有三处被使用,且都是 __OFFSETOFIVAR__ 这种形式,目的是用来取得成员变量的偏移,这是个啥?代码如下:

#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)

简化之后本质如下:

&((TYPE *)0)->MEMBER

可以看到,__OFFSETOFIVAR__ 的本质就是使用 TYPE 对应的结构体去取到 MEMBER 的在这个结构体中的偏移。

&((TYPE *)0)->MEMBER 是成员变量访问逻辑的本质,OFFSET 是静态时期就决定的的,所以这个逻辑会影响到多态,也会影响到 self、super 的某些使用场景,在 Java 中也是如此,不点点在于 Java 中访问成员变量时直接访问,而 OC 是点方法访问成员变量。直接访问 _xxx 是不行的,因为默认 _xxx 是 private 权限修饰符修饰。详情见:this和super、成员变量的访问

这不就是相当于在内存中使用 TYPE 这个模子来找到成员变量相对于这个结构体起始位置的偏移吗!!这更加确信 struct XKStudent 这个结构体的存在,只是这个结构体在哪被定义的呢?是怎么定义的?

这里有两种方法可以从侧面窥探一下这个结构体

第一种,查看运行时结构:

结构体测试

如上图,struct XKStudent 虽然没找到源码,但是却实际存在。这里只是一种简单的猜测来方便理解,因为对编译器实现、Xcode 调试原理没有研究,所以这里可能不正确,自己辩证来看。

第二种,自己定义一个结构体:

struct XKStruct {
  int a;
  int b;
}

这个时候运行编译都是不会报错的,但是使用 -rewrite-objc 指令却报错了:

ERROR

这里显示结构体重复定义就直接证明了 struct XKStudent 的存在。而且编译不报错,也从侧面证明这个结构体只是帮助编译器完成代码的二进制化转换,即生成类的静态数据,完成任务后,结构体也就被删除了。

其实到这里,就可以不需要纠结 struct XKStudent 这个结构体了,而是应该把重心放在 objc 如何在动态链接时将静态数据初始化到,静态数据又是如何生成的;

感兴趣的可以使用 clang -rewrite-legacy-objc main.m -o main.cpp 来看看旧版本的 objc 转 c++ 的源码,这里是有 struct XKStudent的,但是实际意义并不大,所以就不展示了;

3. 另一种尝试

从上面的分析来看,struct XKStudent 更侧重于为编译器或者 ide 提供一些信息,目的是为了生成类的静态数据。那么我们为什么不来看看静态数据和动态数据的联系呢?

objc 源码中,类的初始化逻辑主要在 realizeClassWithoutSwift 函数中。而该函数的数据是从 __objc_classlist 或者 __objc_nlclslist 中来的:

__objc_classlist

上面的类的名称是不是似曾相识?那现在我们换个角度,看看 C++ 源码中何时往这些 section 中添加数据,以此来研究类的本质;

objc 的类加载的过程中,首先读取了 __classlist 中的类列表,然后根据对应的指针去获取静态数据,也就是 ro,最后组装到 rw 上,完成初始化操作。

而静态源码中,插入到 mach-O 的关键代码是:

static struct _class_t *L_OBJC_LABEL_CLASS_$ [2] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
    &OBJC_CLASS_$_XKPerson,
    &OBJC_CLASS_$_XKStudent,
};

如上代码,就是把 OBJC_CLASS_$_XKStudent 这个指针存入了 mach-O 的 __DATA 这个 segment 中的 _objc_classlist section。那这个 L_OBJC_LABEL_CLASS_ 结构体是什么?

extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_XKStudent __attribute__ ((used, section ("__DATA,__objc_data"))) = {
    0, // &OBJC_METACLASS_$_XKStudent,
    0, // &OBJC_CLASS_$_XKPerson,
    0, // (void *)&_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_CLASS_RO_$_XKStudent,
};

而且这个结构体的元类和父类会被填充:

static void OBJC_CLASS_SETUP_$_XKStudent(void ) {
    OBJC_METACLASS_$_XKStudent.isa = &OBJC_METACLASS_$_NSObject;
    OBJC_METACLASS_$_XKStudent.superclass = &OBJC_METACLASS_$_XKPerson;
    OBJC_METACLASS_$_XKStudent.cache = &_objc_empty_cache;
    OBJC_CLASS_$_XKStudent.isa = &OBJC_METACLASS_$_XKStudent;
    OBJC_CLASS_$_XKStudent.superclass = &OBJC_CLASS_$_XKPerson;
    OBJC_CLASS_$_XKStudent.cache = &_objc_empty_cache;
}

总结下来,这个 setup 函数做了几件事:

  1. XKStudent 、XKPerson 和 NSObject 一样,有类、元类;
  2. XKStudent 的元类的 isa 指向 NSObject 对应的元类;
  3. XKStudent 的元类的父类是 XKPerson 对应的元类;
  4. XKStudent 的类对象的 isa 指向 XKStudent 的元类;
  5. XKStudent 的类对象的父类是 XKPerson 的类对象;

说白了,就是这张经典图指向图:

isa/superclass指向

此时我们可以总结一下:

  1. OBJC_CLASS_$_XKStudent 会被存储在 mach-O 中;
  2. OBJC_CLASS_$_XKStudent 会通过 setup 函数初始化;
  3. 初始化函数中设置了元类、类对象的 isa 和 superclass 的指向;

继续看,_class_t 又是什么?这个结构体看上去正好和 objc 中的 objc_class 对应:

objc_class

首先,不需要知道 cache 和 vtable 在 objc 上为什么只用一个 cache 来实现,看注释可以知道,objc 中的 cache 就是表示 cache pointer 和 vtable。

那么接下来就只剩下 ro 和 bits 是否对应了,如果对应,那么静态结构体就和 rw 中的 ro 对应,也就是静态结构体被存储在了 bit 中。

这里需要重点关注两个函数:

struct objc_class : objc_object {
    ....省略...
    class_rw_t *data() {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ....省略...
}

在类的初始化函数 realizeClassWithoutSwift 中会直接使用 ro 对 rw 进行赋值并调用 setData() 传递给 class:

// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);

bits 没什么好说的,就是一个 unsigned long 类型,在 MacOS/iOS 中占 8 个字节,也就是 64 位,感觉就是为了和指针的 size 对应而制定的一个类型。重点是 bits 中这两个方法的具体实现:

// struct class_data_bits_t
class_rw_t* data() {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}

void setData(class_rw_t *newData) {
    // 对3-47位按位取反之后,这44位就都是0了,相当于清空3-47位而不清空0-2位
    // 按位运算,只要一个位1就为1,newData因为8字节对齐原则,0-2位必定为0
    // 整个流程最终的结果就是只修改3-47位,也就是修改rw
    uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
    atomic_thread_fence(memory_order_release);
    bits = newBits;
}

要理解上面的代码,需要知道 FAST_DATA_MASK 是什么。这个宏在 64 位系统下是这么定义的:

#define FAST_DATA_MASK          0x00007ffffffffff8UL

UL 表示 unsign long,可以直接去掉,因为和这个宏定义相关的都是位运算,来看看转换成二进制之后是多少:

FAST_DATA_MASK二进制

一共是 44 + 3 = 47 位,且后三位都是 0;

上述 get 方法中,直接和 FAST_DATA_MASK 进行位运算,其意义就是取 3-47 位的值。

而 set 方法中,做了这么几步:

  1. ~ 表示按位取反,所以 0-2 位变成了 1,相反的,3-47 位变成了 0;
  2. & 表示两个数按位且运算,如果都为 1 时,该位才为 1;
  3. | 表示两个数按位或,如果有一个为 1 ,该位就为 1;

上述三个运算符对应的目的和结果是:

  1. 按位取反之后,和原始数据进行按位且运算,最终保留了原始数据中的 0-2 位,且清空了 3-47 位;
  2. 将上一步的结果和新数据按位或运算,结果就是获取到了新数据;

因为内存时按 8 字节对齐的,所以新数据过来的时候是一个内存地址,这个地址后 3 位必定为 0,所以上述第二步中,因为是 或运算,最后三位的值在经过 set 之后保持原样。

那为什么是 3-47 位?这个其实是和 isa 中的 shiftcls 是对应的:

shiftcls

上述 MACH_VM_MAX_ADDRESS 进行二进制转换之后分别是 47 位和 36 位:

MACH_VM_MAX_ADDRESS二进制转换
MACH_VM_MAX_ADDRESS二进制转换

因为内存地址最少是按 8 字节对齐(OC 中是 16字节),所以后三位可以必定为 0 ,在存储过程中可以直接抹掉,取出时加上就行,所以 shiftcls 的存取时会有对应的位移操作:

// 存储时右移3位抹0
newisa.shiftcls = (uintptr_t)newCls >> 3;
// 读取时左移3位补0
return (Class)((uintptr_t)oldisa.shiftcls << 3);

所以,objc_class 中的 bit 中的 3-47 位存储的就是类的静态数据 ro,也就是 __objc_classlist 表中的指针所指向的数据。而节约出来的 0、1 、2 这三个比特位则可以用来存储三个标志位。三个标志位的存取同样是通过位运算完成,就不再赘述。

至此,可以得出结论:

  1. 类的静态数据存储在 struct objc_class 的 bits 中;
  2. bit 类型是 unsign long,本质上是一个存储指针的容器;
  3. 64 位操作系统下指针的容量有冗余,系统的最大指针地址用 MAX_ADDRESS 表示,一般是 47/36 位;
  4. 内存对齐的本质是提高 CPU 寻址效率,而指针为 8 字节,所以大部分系统都是最少以 8 字节对齐,也就是可以多,但不能少,比如 objc 就是 16 字节对齐;
  5. 基于 8 字节内存对齐原则,最后 3 位必定为 0 ,所以 bit 的最后 3 位可以用来存储其他信息;
  6. 基于以上前提,objc 中才有了 3 位移运算,33/47 位 Mask 这些基本操作;

通过代码分析已经得出结论:_class_t 和 objc 中的 object_class 对应:

_class_t

也就是说 object_class 中的 bit 对应的就是 _class_t 中的 ro,那么静态时期这个 ro 何时被赋值?代码如下:

extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_XKStudent __attribute__ ((used, section ("__DATA,__objc_data"))) = {
    0, // &OBJC_METACLASS_$_XKStudent,
    0, // &OBJC_CLASS_$_XKPerson,
    0, // (void *)&_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_CLASS_RO_$_XKStudent,
};

也就是在 setup 函数被调用之前,这个 ro 就已经被赋值了,这个值就是 _OBJC_CLASS_RO_$_XKStudent,这个结构体定义如下:

static struct _class_ro_t _OBJC_CLASS_RO_$_XKStudent __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    0, //flags
    __OFFSETOFIVAR__(struct XKStudent, _studioName), //instanceStart
    sizeof(struct XKStudent_IMPL), //instanceSize
    (unsigned int)0, //reserved
    0, //ivarlayout
    "XKStudent", //name
    (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_XKStudent, //Method
    0, // protocols
    (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_XKStudent, //ivars
    0, //weakIvarLayout
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_XKStudent,//properties
};

那么 _class_ro_t 是什么呢?而 objc 中又有一个 clsss_ro_t,这两是什么关系?猜测应该也是对应的。来对比一下看看:

对比

这两个完全就是一模一样啊!!!

现在,回到最初的观点:类在初始化时,使用 ro 来初始化 rw。而 ro 就是 clss_ro_t 的类型,这个类型正好和静态编译时期生成的 _class_ro_t 完全重合。所以,这个时候需要来看下 rw 的初始化流程到底是怎么样的,静态数据是如何在初始化阶段被赋值的?

这里就回到了最初的代码:

rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);

上述代码直接将 ro 赋值给 rw 的 ro 属性,然后调用了 cls 的 setData 方法,将 rw 赋值给 cls,完成静态数据初始化。

4. 验证

因为 coding 的本质是机器指令 + 数据,看看运行时内存中的数据是否和静态数据 ro 对应即可验证我们的逻辑。

这里,直接把 objc 中的结构体移植过来使用,看看 XKStudent 的类对象的 name 是否和 _OBJC_CLASS_RO_$_XKStudent 结构体一致即可。

首先把 object_class 移植过来:

struct xk_objc_class : xk_objc_object {
    // Class ISA;
    xkClass superclass;
    xk_cache_t cache;             // formerly cache pointer and vtable
    xk_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

然后依次补足 cache_tclass_data_bits_t

struct bucket_t {
private:
#if __arm64__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif
};

#if __LP64__
typedef uint32_t xk_mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t xk_mask_t;
#endif

struct xk_cache_t {
    struct bucket_t *_buckets;
    xk_mask_t _mask;
    xk_mask_t _occupied;
};

struct xk_class_data_bits_t {
    uintptr_t bits;
};

还要移植 isa_tobjc_object

typedef struct xk_objc_class *xkClass;

union xk_isa_t {
    xk_isa_t() { }
    xk_isa_t(uintptr_t value) : bits(value) { }

    xkClass cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

struct xk_objc_object {
    xk_isa_t isa;
};

最后补足宏定义:

#if !__LP64__

#elif 1

#define XK_FAST_DATA_MASK          0x00007ffffffffff8UL

#else

#define FAST_DATA_MASK          0x00007ffffffffff8UL

#endif

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# else
#   error unknown architecture for packed isa
# endif

上述结构体中,为了方便验证,删除了 method 等比较复杂的结构体。

最后,我们再来写两个 OC 相关的结构体:

struct xk_person : xk_objc_object {
    NSString *name;
};

struct xk_student : xk_person {
    NSString *studioName;
};

此时,就可以写验证的代码了:

int main(int argc, char * argv[]) {
    
    XKStudent *student = [XKStudent new];
    student.name = @"Jack";
    student.studioName = @"Affiliated Middle School of Tsinghua";
    
    struct xk_student *obj = (__bridge struct xk_student *)student;
    xk_isa_t isa = (*obj).isa;
    void *p = (void *)((isa.bits) & ISA_MASK);
    
    NSLog(@"isa:%p",p);
    NSLog(@"name:%p",obj->name);
    NSLog(@"studioName:%p",obj->studioName);

    struct xk_objc_class *cls = (__bridge struct xk_objc_class *)[XKStudent class];
    uintptr_t cls_bits = (*cls).bits.bits;
    struct xk_class_rw_t *cls_rw = (struct xk_class_rw_t *)(cls_bits & XK_FAST_DATA_MASK);
    
    const xk_class_ro_t *cls_ro = (*cls_rw).ro;
    const char *cls_name = (*cls_ro).name;
    
    NSLog(@"class name:%s",cls_name);

    return 0;
}

结果:

isa for instance:0x102fbe0d8
name:Jack
studioName:Affiliated Middle School of Tsinghua
class pointer:0x102fbe0d8
class name:XKStudent

上述代码中:

  1. 创建了一个 XKStudent 的实例;
  2. 使用自定义的 struct xk_student 指针来指向实例对象;
  3. 获取实例对象的 isa,并使用 Mask 获取到真实的 shiftcls 的值;
  4. 打印class、name、studioName 属性;
  5. 获取类对象的地址;
  6. 打印之后可以发现,类对象地址和实例对象的 isa 中存储的 shiftcls 指向一致;
  7. 依次获取到 bits、rw、ro;
  8. 打印 ro 中的 name;

isa 相关知识详见:iOS:isa指针

其实这段代码可以玩一阵子,比如在 MacOS 应用上跑这段代码,如果不支持指针优化,那么 cls 就直接指向类对象。再比如可以继续验证属性、方法等静态结构体,还可以找一找元类、父类对象等,就不再赘述了。

5. 总结

  1. OC 中类首先在静态阶段,被编译器生成相关的结构体。这些结构体包括:实例对象、类对象、元类对象、方法、属性等;
  2. 静态阶段还通过 setup 等方法先进行静态初始化,完成了对象的关联关系的建立,也就是那张经典的 isa、superclass 指向图;
  3. 静态时期初始化完成的结构体会被存储到 mach-O 文件中;
  4. 动态链接时,dyld 和 objc 通过回调的方式触发类的加载流程。
  5. 类的动态初始化中,读取静态时期的 ro 并且赋值给 rw,完成最基本的初始化逻辑;

后续初始化逻辑还干了什么?下回分解......

推荐阅读更多精彩内容