6

+load和+initialize

 3 years ago
source link: https://www.jianshu.com/p/899b36f9b948
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2019.04.17 18:08:09字数 2,628阅读 70

+load方法

当一个类或者分类被加载到Objectie-C的Runtime运行环境中时,会调用它对应的+load方法。对于所有静态库中和动态库中实现了+load方法的类和分类都有效。

当应用启动时,首先要fork进程,然后进行动态链接。+load方法的调用就是在动态链接这个阶段进行的。动态链接结束之后,会执行程序的main函数。

dyld简介

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。整个加载过程可细分为九步:

  • 1、设置运行环境
  • 2、记载共享缓存
  • 3、实例化主程序
  • 4、加载插入的动态库
  • 5、链接主程序
  • 6、链接插入的动态库
  • 7、执行弱符号绑定
  • 8、执行初始化方法
  • 9、查找入口点并返回

步骤8,执行初始化方法。如果看过dyld源码或者源码分析的,可以知道这个步骤是在initializeMainExecutable函数中完成的。dyld会有限初始化动态库,然后初始化主程序。该函数经过系列的执行会进入notifySingle方法,随后会调用到load_images方法,然后会调用到call_load_methods方法。

所以,+load方法会在dyld阶段的执行初始化方法中执行。
多说一点,dyld的初始化顺序:

  • 调用所有Framework中的初始化方法
  • 调用所有的+load方法
  • 调用C++ 的静态初始化方法及C/C++ 中的attribute(constructor)函数
  • 调用给所有链接到目标文件的framework中的初始化方法
+load方法执行顺序
类与类之间的+load方法的执行顺序

有继承关系的类的+load方法的执行顺序,是从基类到子类的;没有继承关系的两个类的+load方法的执行顺序是与编译顺序有关的(Build Phases -> Compile Sources中的顺序)。

类与分类之间+load方法的执行顺序

所有分类的+load方法都在所有类+load方法之后执行,同时又发现所有分类的+load方法的执行顺序与编译顺序有关,与是谁的分类无关,也与一个类有几个分类无关。

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

从这里我们从代码及注释中也能看到:

循环调用call_class_loads方法,直到没有可执行的+load方法

  • 调用call_category_loads方法
  • 重复1->2,直到所有的类和分类的+load方法都执行完毕
  • 所以在这里也能看出来,所有的类的+load方法都执行在分类的+load方法之前。

call_category_loads方法基本上与load_class_loads方法类似,同时还做了一些其他操作。在这里看,我们也就能了解,该函数会获取到所有类及分类的+load方法并执行,所以我们不必手动调用[super load]方法,也能执行到父类的+load方法。

多个镜像中存在+load方法的执行顺序

动态库由于与主工程不是同一个镜像,所以他们之间的输出是分开的,而且动态库的链接要优先于主工程的链接,来保证主工程链接时能链接到期望的动态库。所以动态库的+load方法都要在主工程的+load方法之前执行。其中动态库中类与子类、类与类之间的+load方法的执行顺序,与之前说的一致,这里就不再赘述。

静态库中的类的+load方法,是必须要有代码调用才能加载链接,并且其类的+load方法的执行顺序与编译顺序有关(Link Binary With Libraries的顺序)。

静态库中的分类的+load方法没有调用,其实经常使用静态库开发的同学就知道了,要在主工程的other linker flag中设置-all_load。

如果在+load方法中调用[super load]

我们知道分类如果与类方法重名了,那么在之后调用时,会调用分类的同名方法,如果多个分类都实现了这个方法,那么就会按照编译顺序,最后执行最后编译的分类中的同名方法,执行到分类的+load方法时,会把该方法再次执行一次。
所以为了避免一些不必要的麻烦,我们就不必手动去写[super load]方法,同时也不要自己手动调用[object load]方法。

结合了例子以及dyld、Runtime的源码,弄清楚了+load方法的执行时机,以及顺序。下面就是一些总结

  • 1、+load方法是在dyld阶段的执行初始化方法步骤中执行的,其调用为load_images -> call_load_methods
  • 2、一个类在代码中不主动调用+load方法的情况下,其类、子类实现的+load方法都会分别执行一次
  • 3、父类的+load方法执行在前,子类的+load方法在后
  • 4、在同一镜像中,所有类的+load方法执行在前,所有分类的+load方法执行在后
  • 5、同一镜像中,没有关系的两个类的执行顺序与编译顺序有关(Compile ources中的顺序)
  • 6、同一镜像中所有的分类的+load方法的执行顺序与编译顺序有关(Compile Sources中的顺序),与是谁的分类,同一个类有几个分类无关
  • 7、同一镜像中主工程的+load方法执行在前,静态库的+load方法执行在后。有多个静态库时,静态库之间的执行顺序与编译顺序有关(Link Binary With Libraries中的顺序)
  • 8、不同镜像中,动态库的+load方法执行在前,主工程的+load执行在后,多个动态库的+load方法的执行顺序编译顺序有关(Link Binary With Libraries中的顺序)。
  • 9、当多个分类有相同的方法时,调用顺序为后编译的先调用。

+initialize方法

一个类或者它的子类收到第一条消息(手写代码调用,+load方法不算)之前调用,可以做一些初始化的工作。但该类的+initialize的方法调用,在其父类之后。
Runtime运行时以线程安全的方式将+initialize消息发送给类。也就是说,当一个类首次要执行手动调用的代码之前,会等待+initialize方法执行完毕后,再调用该方法。

这里需要注意的一点:

当子类没有实现+initialize或者子类在+initialize中显式的调用了[super initialize],那么父类的+initialize方法会被调用多次。如果希望避免某一个类中的+initialize方法被调用过多次,可以使用下面的方法来实现:

+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

因为+initialize是以阻塞方式调用的,所以很重要的一点就是将方法实现限制为可能最小的工作量。

本文主要通过官方文档、例子以及Runtime源码,分析了+initialize方法的调用,总结如下:

  • 1、当代码执行到一个类第一次调用方法时,会调用这个类的+initialize方法
  • 2、在调用自身类的+initialize方法之前,会判断其父类链上是否有类还没有执行+initialize方法,如果没有执行,那么执行。所以所有父类的+initialize方法都执行在前,子类的+initialize执行在后。
  • 3、如果一个类有多个分类都实现了+initialize方法,那么会执行编译顺序的最后一个分类实现的+initialize方法
  • 4、当一个类实现了+initialize方法,但是子类没有实现+initialize或者子类在实现+initialize方法中显式的调用的[super initialize]方法,那么该类的+initialize方法会调用多次,如果不想该方法被多次调用,可以在该类的+initialize方法通过if (self = [ClassName self])进行判断来避免多次调用。

+load方法与+initialize方法的区别

+load方法: 根据函数地址直接调用
+initialize方法: 是通过objc_msgSend调用

+load方法:是Runtime加载类、分类的时候调用(如果不显式调用,只会调用一次)
+initialize方法:是类第一次接收到消息的时候调用(如果不显式调用,可能存在调用多次的风险)

+load方法

  • 先调用类的+load方法,再调用分类的+load方法
  • 有继承关系的类,先调用父类的+load,后调用子类的+load方法
  • 没有继承关系的类,会按照编译顺序来执行+load方法
  • 所有的分类,都按照编译顺序来执行+load方法
+initialize方法
  • 先调用父类的+initialize方法,后调用子类的+initialize方法
  • 如果一个类有分类,那么会调用最后编译的分类实现的+initialize方法
  • 通过消息机制调用,当子类没有实现+initialize方法时,会调用父类的initialize方法
  • load方法一个类只会调用一次(除去手动调用),而调用的数序是,从superclass -> class -> category,category里面的顺序是先编译,先调用
  • initialize方法,一个类可能会调用多次,如果子类没有实现initialize方法,当第一次使用此类的时候,会调用superclass。而调用的顺序是,superclass -> 实现initialize的category 或者 实现了initialize方法(没有category实现initialize) 或者 superclass的initialize (没有子类和category实现initialize方法)

参考:
+load方法的执行顺序你了解么?
+initialize方法的调用时机


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK