runtime的那些事(三)——NSObject初始化 load 与 initialize

从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() 函数执行前,依次做了以下这些工作:

  1. 系统加载App自身所有的 可执行文件(Mach-O文件),并获取 dyld 的路径(dyld是专门用来加载动态链接库的);
  2. dyld 初始化运行环境,并开启 dyld 缓存策略(主要区分于App的冷启动与热启动),从可执行文件的依赖顺序开始,递归加载所有依赖的动态链接库,所有依赖库通过 dyld 内部实现将 Mach-O 文件实例化为 image 镜像文件。

注:动态链接库包括:所有系统 framework、系统级别的 libSystem(libdispatch、libsystem_blocks等)、加载 Objective-C runtime 的 libobjc(即Objective-C runtime 初始化)

  1. dyld 对所有依赖库初始化后,此时 runtime 会对项目中所有类进行类结构初始化,然后调用所有类的 load 方法
  2. dyld最后会返回 main() 函数地址,main() 函数被调用,随后便进入熟悉的程序入口,默认从 AppDelegate 类开始。

 该章节仅仅是对 load 方法加载进行分析,所以关于 dyld 动态链接库并不展开。
 在一个类的 load 方法中添加断点,编译运行后,在控制台 lldb 中调用 bt 命令,可查看到完整的堆栈调用信息。

runtime的那些事(三)——NSObject初始化 load 与 initialize_第1张图片

堆栈信息中,在 dyld 加载完动态链接库之后,类的 load 方法之前,runtime 调用了两个函数: load_imagescall_load_methods

1. load_images

先来看下 load_images

runtime的那些事(三)——NSObject初始化 load 与 initialize_第2张图片

 第一步,会快速依次检查类与分类中是否存在不带锁的 load 方法,这是在 runtime 中的注释,讲真的,不带锁的 load 方法,没看懂。带着好奇心去看一看 bool hasLoadMethods(const headerType *mhdr) 函数实现,发现了 _getObjc2NonlazyClassList_getObjc2NonlazyCategoryList
runtime的那些事(三)——NSObject初始化 load 与 initialize_第3张图片

程序初始化过程,所有 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) 函数。

runtime的那些事(三)——NSObject初始化 load 与 initialize_第4张图片

 在 void prepare_load_methods(const headerType *mhdr) 函数中,遍历 _getObjc2NonlazyClassList 函数里已加载 load 方法的类,并先获取当前处于活动状态的类指针(因为类指针可能会指向已重新分配的类结构;并且会对 weak 链接的忽略,返回 nil ),再递归去查找当前处于有效连接的类以及没有调用 load 方法的父类,添加至可执行 load 方法加载数组中。而且,为了保证父类要在子类前调用 load 方法,是通过 static void schedule_class_load(Class cls) 函数递归来实现的。
runtime的那些事(三)——NSObject初始化 load 与 initialize_第5张图片

 最后通过 void add_class_to_loadable_list(Class cls) 函数,将已处于有效连接状态的类添加至可加载 load 方法的类数组中,并且会将对应类的 load 方法 IMP 添加维护进一个专门维护 load 方法数组中。函数声明也可以发现,通过递归让类的超类先执行 void add_class_to_loadable_list(Class cls) 函数,当确保超类没有实现 load 方法,就将超类添加至可加载 load 方法数组,随后再将该类添加至数组中。
runtime的那些事(三)——NSObject初始化 load 与 initialize_第6张图片

 当非懒加载类遍历添加至可执行 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 函数声明。

runtime的那些事(三)——NSObject初始化 load 与 initialize_第7张图片

关于 call_load_methods 函数的作用,在 runtime 源码已经给了很好的说明。

  1. 优先调用所有类的 load 方法,再去执行分类的 load 方法;
  2. 父类 load 方法优先于子类的执行;
  3. 该函数声明是允许多次执行的,因为在 load 加载过程中会触发更多的 image 镜像文件映射,而load 方法的调用是通过 dyld(动态链接库) dyld_register_image_state_change_handler ,当每次有新的镜像文件添加时触发(此处dyld的调用不展开);
  4. 通过 do while 循环一直重复去调用类 load 方法,直到可加载 load 方法的类不再有;
  5. 分类的 load 方法只会执行一次,以确保“父类优先”的调用排序,即使分类加载时会触发新的可加载类;
  6. 在 do while 循环执行 load 方法过程中,为了保证线程安全,loadMethodLock 必须被调用者持有,其它任何锁不能被持有。

在 do while 循环外面,使用了 autoreleasePool 进行管理。每当循环执行完毕时,会及时清理中间过程产生的临时变量以及内存资源消耗。

call_class_loads() 与 call_category_loads()

上述两个方法分别是遍历调用类与分类 load 方法

runtime的那些事(三)——NSObject初始化 load 与 initialize_第8张图片
call_class_loads()方法实现

 在调用 load 方法时,并没有通过 objc_msgSend() 方法来发送消息,而是直接获取了对应类的 load 方法内存地址来调用 (*load_method)(cls, SEL_load);,该调用方式最显著的特性,就是类、父类、分类之间调用 load 方法不会互相影响,当实现了类的 load 方法时,不会主动调用父类的 load 方法。换句话说,也就是实现了类的 load 方法,不需调用 [super load]; 方法。

runtime的那些事(三)——NSObject初始化 load 与 initialize_第9张图片
call_category_loads() 函数实现

而在 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 方法的特性。

  1. 加载 load 方法是在程序初始化阶段,runtime 初始化过程 load_images 中执行的;
  2. 父类的 load 方法一定会优先于子类的 load 方法执行;
  3. 所有类的 load 方法执行在前,分类的 load 方法后续执行;
  4. 一个类即使不主动代码调用 load 方法,其类、子类都会执行一次 load 方法;
  5. 不需要在 load 方法中调用 [super load] 方法,内部会遍历递归向上查找父类并执行其 load 方法;
  6. 主工程中的类 load 方法加载是在 dyld 动态链接库最后阶段调用,意味着项目中引入的动态库 load 方法会优先于主工程中的类 load 方法执行;

当然 load 方法还有一些其它特性,比如:
同一 image 镜像文件下,没有关系的两个类调用 load 方法的顺序,是按照类文件在 Compile Sources 中的顺序执行;
同一 image 镜像文件下,每个类的分类若实现了 load 方法,都会去执行,执行顺序也是按照分类文件在 Compile Sources 中的顺序;


二、initialize 方法

+(void) initialize;

关于 initialize 方法的调用时机,什么时候会调用 initialize 方法?
 当引入一个类却不对它做任何事的时候,并不会触发 initialize 方法执行;只有对该类进行第一次消息发送,即触发调用 objc_msgSend() 方法时,才会去执行。

runtime的那些事(三)——NSObject初始化 load 与 initialize_第10张图片
调用 initialize 方法

关于 IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) ,其作用是查找方法的实现 IMP,在类的消息发送流程中有着举足轻重的地位。
在上述源码中,当类第一次接收到消息时,会判断出需要 initialize 方法初始化而且没有执行过 initialize 方法,则会去执行 void _class_initialize(Class cls) 方法,并且对 initialize 方法执行加锁保护。
runtime的那些事(三)——NSObject初始化 load 与 initialize_第11张图片

void _class_initialize(Class cls) 方法中,首先会去递归检查父类是否已经执行过 initialize 方法。
然后,判断当前类的 flags 掩码位运算不是 RW_INITIALIZEDRW_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 方法)

小结

  1. initialize 在类第一次接收到消息时调用,也就是 objc_msgSend(),其本质也是通过 objc_msgSend() 方法调用;
  2. 在类初始化过程中,会优先调用父类的 initialize,再调用本类的 initialize;
  3. 若本类没有实现 initialize,而父类实现了 initialize ,那么本类的初始化会去调用并继承父类的 initialize 方法,通过 superclass 到父类中查找,意味着父类的 initialize 方法可能会多次调用;
  4. 本类的 initialize 方法实现会覆盖之前继承自父类的 initialize 方法;
  5. 在重写 initialize 方法时,不需要调用 [super initialize] 方法,因为其内部会自动递归向上查找执行父类 initialize 方法;
  6. 分类中的 initialize 方法会优先执行,本类中的 initialize 方法不会再调用,究其原因是 obj_msgSend() 方法机制;

关于 initialize 的一些其它特性:
当有多个分类实现了 initialize 方法时,只会执行最后一个分类的(最后一个是指在 Compile Sources 中排列顺序最靠后的分类);


后记:
 关于类的初始化 load 与 initialize 方法就先写到这里。在整理写作过程中,我自己也发现了有很多还需要待完善的知识点,比如:每个类、分类 load 方法是何时、如何加载进可加载 load 列表中,dyld 动态链接库对 image 镜像文件的操作流程。后续会不断补充,若是文章中出现不准确的地方还请多多指点。


该文章首次发表在 :我只不过是出来写写代码 博客,并自动同步至 腾讯云:我只不过是出来写写iOS 博客

你可能感兴趣的:(runtime的那些事(三)——NSObject初始化 load 与 initialize)