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

作者: 我只不过是出来写写代码 | 来源:发表于2019-05-29 10:31 被阅读2476次

    从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 命令,可查看到完整的堆栈调用信息。


    堆栈信息中,在 dyld 加载完动态链接库之后,类的 load 方法之前,runtime 调用了两个函数:load_imagescall_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 源码已经给了很好的说明。
    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 方法

    call_class_loads()方法实现
     在调用 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 方法的特性。

    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() 方法时,才会去执行。

    调用 initialize 方法
    关于 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_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

        本文链接:https://www.haomeiwen.com/subject/vsrioqtx.html