从runtime源代码层面去研究下NSObject类初始化相关方法:load、initialize,以及在调用时内部做了什么
目录
一、load 方法
1. load_images
2. call_load_methods
二、initialize 方法
一、load 方法
+(void) load;
作为iOS开发,多少都与 load 方法打过交道——在程序 main
函数调用前,类被注册加载到内存时,load
方法会被调用。也就是说每个类的 load
方法都会被调用一次。
在该方法中,我们最常用到的场景,就是使用 runtime 提供的交换函数 OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
,去改变系统方法行为并添加自定义的行为。
但若要了解 load 方法内部实现流程,还得从iOS程序启动流程开始说起。
在程序的 main()
函数执行前,依次做了以下这些工作:
- 系统加载App自身所有的
可执行文件
(Mach-O文件),并获取dyld
的路径(dyld是专门用来加载动态链接库的); -
dyld
初始化运行环境,并开启dyld 缓存策略
(主要区分于App的冷启动与热启动),从可执行文件的依赖顺序开始,递归加载所有依赖的动态链接库,所有依赖库通过dyld
内部实现将 Mach-O 文件实例化为image
镜像文件。
注:动态链接库包括:所有系统 framework、系统级别的 libSystem(libdispatch、libsystem_blocks等)、加载 Objective-C runtime 的 libobjc(即Objective-C runtime 初始化)
- 当
dyld
对所有依赖库初始化后,此时 runtime 会对项目中所有类进行类结构初始化,然后调用所有类的 load 方法
; - dyld最后会返回
main()
函数地址,main()
函数被调用,随后便进入熟悉的程序入口,默认从AppDelegate
类开始。
该章节仅仅是对 load 方法加载进行分析,所以关于 dyld 动态链接库并不展开。
在一个类的 load 方法中添加断点,编译运行后,在控制台 lldb 中调用 bt
命令,可查看到完整的堆栈调用信息。
堆栈信息中,在 dyld 加载完动态链接库之后,类的
load
方法之前,runtime 调用了两个函数:
load_images
与
call_load_methods
。
1. load_images
先来看下 load_images
第一步,会快速依次检查类与分类中是否存在不带锁的 load 方法,这是在 runtime 中的注释,讲真的,不带锁的 load 方法,没看懂。带着好奇心去看一看
bool hasLoadMethods(const headerType *mhdr)
函数实现,发现了
_getObjc2NonlazyClassList
与
_getObjc2NonlazyCategoryList
。
程序初始化过程,所有 class 类实现都被存储在 image 镜像文件中一个二进制列表里,并且会在列表中拥有一个引用,这个二进制列表会允许 runtime 去追踪检索访问已存储的类,但所有类并不会都在程序启动时就要实现。因此当一个类实现 load 方法时,也会在这个二进制列表添加一个引用索引,让 runtime 去追踪访问。而这个二进制列表存储于 image 镜像文件的 "__DATA, __objc_nlclslist, regular, no_dead_strip" 部分(看来后续文章要去深入了解dyld动态链接库了)
在
_getObjc2NonlazyClassList
检索类数组中已经实现
load
方法的类,也就是非懒加载类。非懒加载类一定会在程序启动时实现 load 方法,与之对应的懒加载类却并没有实现。懒加载类会延迟到类第一次接收到消息时加载 load 方法。同理,
_getObjc2NonlazyCategoryList
作用于分类,与
_getObjc2NonlazyClassList
功能相同。
当检索懒加载类时,则需要用到
_getObjc2ClassList
与
_getObjc2CategoryList
,分别检索所有类(包括非懒加载类、懒加载类)、分类扩展(包括非懒加载类、懒加载类)。
因此,
bool hasLoadMethods(const headerType *mhdr)
函数作用,是查询所有非懒加载类、类扩展数组中是否存在已加载 load 方法。但为什么该函数在 runtime 中被注释为:快速扫描不带锁的 load 方法。对于非懒加载类的 load 方法在 runtime 中被定义为不带锁的 load 方法?到现在还一直有这个疑问。
第二步,当判断存在非懒加载类、类扩展的 load 方法时,会先用互斥锁上锁该线程,并执行 void prepare_load_methods(const headerType *mhdr)
函数。
在
void prepare_load_methods(const headerType *mhdr)
函数中,遍历
_getObjc2NonlazyClassList
函数里已加载 load 方法的类,并先获取当前处于活动状态的类指针(因为类指针可能会指向已重新分配的类结构;并且会对 weak 链接的忽略,返回 nil ),再递归去查找当前处于有效连接的类以及没有调用 load 方法的父类,添加至可执行 load 方法加载数组中。而且,为了保证父类要在子类前调用 load 方法,是通过
static void schedule_class_load(Class cls)
函数递归来实现的。
最后通过
void add_class_to_loadable_list(Class cls)
函数,将已处于有效连接状态的类添加至可加载
load
方法的类数组中,并且会将对应类的 load 方法 IMP 添加维护进一个专门维护 load 方法数组中。函数声明也可以发现,通过递归让类的超类先执行
void add_class_to_loadable_list(Class cls)
函数,当确保超类没有实现 load 方法,就将超类添加至可加载 load 方法数组,随后再将该类添加至数组中。
当非懒加载类遍历添加至可执行 load 方法的类数组后,再对所有的分类也执行相同的操作,并将分类以及对应的方法 IMP 维护至对应数组中。但是在分类的遍历过程中,会首先对分类对应的类进行
static Class realizeClass(Class cls)
函数操作,将类进行初始化。关于
static Class realizeClass(Class cls)
函数的作用,前篇文章 runtime的那些事(二)——NSObject数据结构已做介绍,为了能够让类对应的分类信息加载至类结构体中,必须先要将类进行初始化。
当非懒加载类、分类信息,以及对应 load 方法 IMP 准备完成后,接下来就会进入到
call_load_methods()
函数中。
2. call_load_methods
call_load_methods
函数声明。
关于
call_load_methods
函数的作用,在 runtime 源码已经给了很好的说明。
- 优先调用所有类的 load 方法,再去执行分类的 load 方法;
- 父类 load 方法优先于子类的执行;
- 该函数声明是允许多次执行的,因为在 load 加载过程中会触发更多的 image 镜像文件映射,而load 方法的调用是通过 dyld(动态链接库)
dyld_register_image_state_change_handler
,当每次有新的镜像文件添加时触发(此处dyld的调用不展开); - 通过 do while 循环一直重复去调用类 load 方法,直到可加载 load 方法的类不再有;
- 分类的 load 方法只会执行一次,以确保“父类优先”的调用排序,即使分类加载时会触发新的可加载类;
- 在 do while 循环执行 load 方法过程中,为了保证线程安全,
loadMethodLock
必须被调用者持有,其它任何锁不能被持有。
在 do while 循环外面,使用了 autoreleasePool 进行管理。每当循环执行完毕时,会及时清理中间过程产生的临时变量以及内存资源消耗。
call_class_loads() 与 call_category_loads()
上述两个方法分别是遍历调用类与分类 load 方法
在调用 load 方法时,并没有通过
objc_msgSend()
方法来发送消息,而是直接获取了对应类的 load 方法内存地址来调用
(*load_method)(cls, SEL_load);
,该调用方式最显著的特性,就是类、父类、分类之间调用 load 方法不会互相影响,当实现了类的 load 方法时,不会主动调用父类的 load 方法。换句话说,也就是实现了类的 load 方法,不需调用
[super load];
方法。
而在
call_category_loads()
方法中,与
call_class_loads()
方法调用稍有不同。
- 先将可加载 load 方法的分类数组复制了一份相同结构体数组,命名为
cats
; - 在 cats 数组遍历加载分类 load 方法后(同样是通过直接获取 load 方法的内存地址来调用),会从 cats 中删除已加载 load 方法的分类;
- 再次检查
loadable_categories
数组中是否有新的可加载 load 方法的分类,若存在,先判断分类数组内存是否已被全部占用,若全部占用则在当前数组内存的基础上进行扩充,调用realloc
进行动态分配内存修改,再将新的分类添加至 cats 中; - 销毁原有的
loadable_categories
- 若不存在新的分类加入,则销毁 cats 数组,
loadable_categories
相关参数全部置为初始状态,并 return NO,代表着全部分类已加载 load 方法完成;若存在新的分类加入 cats 数组,则会将数组 cats 赋值给loadable_categories
,并在最后return YES,代表着有新的分类加入并需要加载其 load 方法。
小结
从 runtime 源码层面去研究 load 方法的加载,从中也得到一些关于 load 方法的特性。
- 加载 load 方法是在程序初始化阶段,runtime 初始化过程
load_images
中执行的; - 父类的 load 方法一定会优先于子类的 load 方法执行;
- 所有类的 load 方法执行在前,分类的 load 方法后续执行;
- 一个类即使不主动代码调用 load 方法,其类、子类都会执行一次 load 方法;
- 不需要在 load 方法中调用
[super load]
方法,内部会遍历递归向上查找父类并执行其 load 方法; - 主工程中的类 load 方法加载是在 dyld 动态链接库最后阶段调用,意味着项目中引入的动态库 load 方法会优先于主工程中的类 load 方法执行;
当然 load 方法还有一些其它特性,比如:
同一 image 镜像文件下,没有关系的两个类调用 load 方法的顺序,是按照类文件在 Compile Sources 中的顺序执行;
同一 image 镜像文件下,每个类的分类若实现了 load 方法,都会去执行,执行顺序也是按照分类文件在 Compile Sources 中的顺序;
二、initialize 方法
+(void) initialize;
关于 initialize 方法的调用时机,什么时候会调用 initialize 方法?
当引入一个类却不对它做任何事的时候,并不会触发 initialize 方法执行;只有对该类进行第一次消息发送,即触发调用 objc_msgSend()
方法时,才会去执行。
关于
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
,其作用是查找方法的实现 IMP,在类的消息发送流程中有着举足轻重的地位。
在上述源码中,当类第一次接收到消息时,会判断出需要 initialize 方法初始化而且没有执行过 initialize 方法,则会去执行
void _class_initialize(Class cls)
方法,并且对 initialize 方法执行加锁保护。
在
void _class_initialize(Class cls)
方法中,首先会去递归检查父类是否已经执行过 initialize 方法。
然后,判断当前类的 flags 掩码位运算不是
RW_INITIALIZED
与
RW_INITIALIZING
时,设置其 flags 掩码位为
RW_INITIALIZING
,标记为需要执行 initialize 方法。并使用原子保护,防止重复执行 initialize 方法。
最后,去执行
callInitialize(cls);
方法,而这个方法的实现也非常简单,
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
。区别于 load 方法的执行,使用
objc_msgSend()
消息发送执行
SEL_initialize
selector,并没有像 load 方法一样直接获取 selector 的内存地址来调用。既然是使用了
objc_msgSend
走消息发送流程,当子类没有实现时,会调用继承的父类实现;若分类实现了 initialize 方法,那么就会优先执行分类的(本类中的 initialize 方法实现并没有被覆盖,依然存在于类信息中,只是因为分类实现了并优先执行分类的 initialize 方法)
小结
- initialize 在类第一次接收到消息时调用,也就是
objc_msgSend()
,其本质也是通过objc_msgSend()
方法调用; - 在类初始化过程中,会优先调用父类的 initialize,再调用本类的 initialize;
- 若本类没有实现 initialize,而父类实现了 initialize ,那么本类的初始化会去调用并继承父类的 initialize 方法,通过 superclass 到父类中查找,意味着父类的 initialize 方法可能会多次调用;
- 本类的 initialize 方法实现会覆盖之前继承自父类的 initialize 方法;
- 在重写 initialize 方法时,不需要调用
[super initialize]
方法,因为其内部会自动递归向上查找执行父类 initialize 方法; - 分类中的 initialize 方法会优先执行,本类中的 initialize 方法不会再调用,究其原因是
obj_msgSend()
方法机制;
关于 initialize 的一些其它特性:
当有多个分类实现了 initialize 方法时,只会执行最后一个分类的(最后一个是指在 Compile Sources 中排列顺序最靠后的分类);
后记:
关于类的初始化 load 与 initialize 方法就先写到这里。在整理写作过程中,我自己也发现了有很多还需要待完善的知识点,比如:每个类、分类 load 方法是何时、如何加载进可加载 load 列表中,dyld 动态链接库对 image 镜像文件的操作流程。后续会不断补充,若是文章中出现不准确的地方还请多多指点。
该文章首次发表在 :我只不过是出来写写代码 博客,并自动同步至 腾讯云:我只不过是出来写写iOS 博客