在 iOS类加载流程(一):类加载流程的触发
中已经知道两个关键函数 map_images()
和 load_images()
的触发逻辑了。但是现在直接看 map_images()
会一脸懵逼。
这里直接看 realizeClassWithoutSwift
函数,我们只需要知道,这个函数是 objc 进行类的懒加载和非懒加载必须调用的方法,也是类初始化中的关键函数。理解了这个函数再去看主流程会比较清晰;
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
}
上述代码做了几件事:
- 变量声明和一些条件判断,没什么好说的;
- 为 rw 分配内存并且将 ro 赋值给 rw;
- 设置了一些标志位;
这里需要提一下这段代码:
// classref_t is unremapped class_t*
typedef struct classref * classref_t;
根据注释可知道 struct classref
表示没有被 map 的 class_t*
。而 class_t*
就是运行时的类结构体。但是,怎么证明或者体现呢?这里可以搜一下 objc 中 classref_t
的使用,基本上会有这样一段代码:
上图可以看到 classref
被强制转化成了 Class,其他几处使用到 classref_t
的地方也都是这样,总结下来两个共同点:
-
classref_t
指向的都是从 mach-O 文件中的数据(类的静态数据); - 都被强制转化成了 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);
这里有几个疑问需要解开:
- ro 和 rw 是什么?
-
setData()
做了什么?
关于第一点,先直接说结论:
- ro 就是
class_ro_t
结构体,表示编译时期生成的类的静态结构,直接记录在 mach-O 文件类,被__objc_classlist
和__objc_nlclslist
这两个 section 记录并映射。而 ro 的生成过程是在静态编译链接时期,可以通过 OC 转换后的 C/C++ 代码来看到表示数据的静态时期的结构体; - 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 中任何一个实例变量。
但是,这里有两个疑问:
- 源码中有使用到
struct XKStudent
,而XKStudent
是别名,已经包含了 struct,所以不需要带 struct,那这个struct XKStudent
必定存在,在哪被声明的呢?结构体又是怎样? - 除了 isa 指针外,每个实例变量都有一份自己的成员变量,而
XKStudent
是别名,本质是struct objc_object
,而struct objc_object
内部只有一个 isa 指针,如何表示类的结构呢?
因此,必定还存在一个 struct XKStudent
的结构体定义。
可以先不管 OC 中方法寻找流程,但是最少需要这么一个结构体来让编译器或者 ide 知道这个类有哪些成员属性,进而进行内存分配或者是代码提示。
XKStudent
在源码中有三种用法:
- XKStudent;
- struct XKStudent;
- 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
这个结构体只在这一个地方被使用到,而且只是用来计算 size,所以,大概率这个结构体实际上并没有被使用到,完成了 size 的计算使命之后就不再使用了~~~
还有就是 struct 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
指令却报错了:
这里显示结构体重复定义就直接证明了 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
中来的:
上面的类的名称是不是似曾相识?那现在我们换个角度,看看 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 函数做了几件事:
- XKStudent 、XKPerson 和 NSObject 一样,有类、元类;
- XKStudent 的元类的 isa 指向 NSObject 对应的元类;
- XKStudent 的元类的父类是 XKPerson 对应的元类;
- XKStudent 的类对象的 isa 指向 XKStudent 的元类;
- XKStudent 的类对象的父类是 XKPerson 的类对象;
说白了,就是这张经典图指向图:
isa/superclass指向此时我们可以总结一下:
-
OBJC_CLASS_$_XKStudent
会被存储在 mach-O 中; -
OBJC_CLASS_$_XKStudent
会通过 setup 函数初始化; - 初始化函数中设置了元类、类对象的 isa 和 superclass 的指向;
继续看,_class_t
又是什么?这个结构体看上去正好和 objc 中的 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
,可以直接去掉,因为和这个宏定义相关的都是位运算,来看看转换成二进制之后是多少:
一共是 44 + 3 = 47 位,且后三位都是 0;
上述 get 方法中,直接和 FAST_DATA_MASK
进行位运算,其意义就是取 3-47 位的值。
而 set 方法中,做了这么几步:
- ~ 表示按位取反,所以 0-2 位变成了 1,相反的,3-47 位变成了 0;
- & 表示两个数按位且运算,如果都为 1 时,该位才为 1;
- | 表示两个数按位或,如果有一个为 1 ,该位就为 1;
上述三个运算符对应的目的和结果是:
- 按位取反之后,和原始数据进行按位且运算,最终保留了原始数据中的 0-2 位,且清空了 3-47 位;
- 将上一步的结果和新数据按位或运算,结果就是获取到了新数据;
因为内存时按 8 字节对齐的,所以新数据过来的时候是一个内存地址,这个地址后 3 位必定为 0,所以上述第二步中,因为是 或运算,最后三位的值在经过 set 之后保持原样。
那为什么是 3-47 位?这个其实是和 isa 中的 shiftcls
是对应的:
上述 MACH_VM_MAX_ADDRESS
进行二进制转换之后分别是 47 位和 36 位:
因为内存地址最少是按 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 这三个比特位则可以用来存储三个标志位。三个标志位的存取同样是通过位运算完成,就不再赘述。
至此,可以得出结论:
- 类的静态数据存储在
struct objc_class
的 bits 中; - bit 类型是 unsign long,本质上是一个存储指针的容器;
- 64 位操作系统下指针的容量有冗余,系统的最大指针地址用 MAX_ADDRESS 表示,一般是 47/36 位;
- 内存对齐的本质是提高 CPU 寻址效率,而指针为 8 字节,所以大部分系统都是最少以 8 字节对齐,也就是可以多,但不能少,比如 objc 就是 16 字节对齐;
- 基于 8 字节内存对齐原则,最后 3 位必定为 0 ,所以 bit 的最后 3 位可以用来存储其他信息;
- 基于以上前提,objc 中才有了 3 位移运算,33/47 位 Mask 这些基本操作;
通过代码分析已经得出结论:_class_t
和 objc 中的 object_class
对应:
也就是说 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_t
和 class_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_t
和 objc_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
上述代码中:
- 创建了一个 XKStudent 的实例;
- 使用自定义的
struct xk_student
指针来指向实例对象; - 获取实例对象的 isa,并使用 Mask 获取到真实的 shiftcls 的值;
- 打印class、name、studioName 属性;
- 获取类对象的地址;
- 打印之后可以发现,类对象地址和实例对象的 isa 中存储的 shiftcls 指向一致;
- 依次获取到 bits、rw、ro;
- 打印 ro 中的 name;
isa 相关知识详见:iOS:isa指针
其实这段代码可以玩一阵子,比如在 MacOS 应用上跑这段代码,如果不支持指针优化,那么 cls 就直接指向类对象。再比如可以继续验证属性、方法等静态结构体,还可以找一找元类、父类对象等,就不再赘述了。
5. 总结
- OC 中类首先在静态阶段,被编译器生成相关的结构体。这些结构体包括:实例对象、类对象、元类对象、方法、属性等;
- 静态阶段还通过 setup 等方法先进行静态初始化,完成了对象的关联关系的建立,也就是那张经典的 isa、superclass 指向图;
- 静态时期初始化完成的结构体会被存储到 mach-O 文件中;
- 动态链接时,dyld 和 objc 通过回调的方式触发类的加载流程。
- 类的动态初始化中,读取静态时期的 ro 并且赋值给 rw,完成最基本的初始化逻辑;
后续初始化逻辑还干了什么?下回分解......
网友评论