+load方法的执行顺序你了解么?

可能+load方法应该是每个iOS开发同学都非常熟悉的方法,而且面试中+load方法相关的面试提也是非常常见,但你了解的+load方法真的跟实际上的一样么?

看文章之前先思考几个问题

  1. +load方法在什么时候?
  2. +load方法是如何执行的?
  3. 一个类的+load方法会执行几次?
  4. 类和分类的+load方法的执行顺序?
  5. 同一个类的不同分类的+load方法的执行顺序?
  6. 父类和子类的+load方法的执行顺序?
  7. 没有继承关系的两个类的+load方法的执行顺序?
  8. 静态库、动态库中的+load方法与主程序的+load的执行顺序?
  9. 为什么我们在+load方法中不必写[super load]

如果有些问题不确定或者不知道,那么就往下看,我们一起来解决这些问题。

+load方法的执行时机

官方文档是这样描述的:

Invoked whenever a class or category is added to the Objective-C runtime。The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.

当一个类或者分类被加载到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方法。我们之前分析过dyld,如果感兴趣请看之前发表的这篇博客。如果不想研究源码也没关系,我们随便写一个工程,新建一个类并实现+load方法,打断点定位,我们也能得到下图的调用栈。

image

所以到这里,我们得到了第一个问题的答案:+load方法会在dyld阶段的执行初始化方法中执行。

多说一点,dyld的初始化顺序:

  1. 调用所有Framework中的初始化方法
  2. 调用所有的+load方法
  3. 调用C++ 的静态初始化方法及C/C++ 中的attribute(constructor)函数
  4. 调用给所有链接到目标文件的framework中的初始化方法

+load方法的执行顺序

官方文档中提到了+load方法的执行顺序

  1. 一个类的+load方法调用在它的父类的+load方法之后
  2. 一个分类的+load方法调用在它本身类的+load方法之后

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

我们写一个demo来验证一下。新建一个iOS工程,然后新建一个Person

@interface Person : NSObject

@end

@implementation Person

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end

然后新建一个Student类继承Person类。

@interface Student : Person

@end

@implementation Student

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end

再新建一个HighSchoolStudent类继承Student

@interface HighSchoolStudent : Student

@end

@implementation HighSchoolStudent

+ (void)load
{    
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end

到这里,我们看到了一条继承链。运行程序,得到结果

---- 0x10b04c0c0 +[Person load]
---- 0x10b04c160 +[Student load]
---- 0x10b04c1b0 +[HighSchoolStudent load]

结果如预期,接下来,我们增加一个Animal

@interface Animal : NSObject

@end

@implementation Animal

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end

现在看一下结果

---- 0x1094930e8 +[Person load]
---- 0x109493138 +[Animal load]
---- 0x109493188 +[Student load]
---- 0x1094931d8 +[HighSchoolStudent load]

我们发现,Animal类的+load方法也调用了,但是它的调用顺序,我们还不知道是如何的。这个时候,我们去Build Phases中的Compile Sources中看一下。

image

我们发现这里面的四个.m顺序与+load方法打印的顺序一致。那么我们把这里的顺序全部调转,然后再看下打印结果。

image

---- 0x1010331d8 +[Person load]
---- 0x101033138 +[Student load]
---- 0x1010330e8 +[HighSchoolStudent load]
---- 0x101033188 +[Animal load]

我们发现Animal的输出变到最后了,那我们再次修改顺序。

image

查看打印结果

---- 0x10803e0e8 +[Animal load]
---- 0x10803e1d8 +[Person load]
---- 0x10803e188 +[Student load]
---- 0x10803e138 +[HighSchoolStudent load]

这样我们能得出结论:有继承关系的类的+load方法的执行顺序,是从基类到子类的;没有继承关系的两个类的+load方法的执行顺序是与编译顺序有关的(Build Phases -> Compile Sources中的顺序)

了解Mach-o文件布局的人应该明白,先编译的类就会在可执行文件的前面,编译顺序也体现到了没有继承关系的两个类的+load方法的执行顺序中了。

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

看完了类与类之间+load方法的执行顺序,我们来看看类与分类,以及分类与分类之间的+load方法的执行顺序。

在刚才例子的基础上,我们在新建Person的两个分类Test1Test2,以及Student的两分类Test1Test2,和Animal的分类Test

@interface Person (Test1)

@end

@implementation Person (Test1)

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end


@interface Person (Test2)

@end

@implementation Person (Test2)

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end


@interface Student (Test1)

@end

@implementation Student (Test1)

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end


@interface Student (Test2)

@end

@implementation Student (Test2)

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end


@interface Animal (Test)

@end

@implementation Animal (Test)

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end

运行,看下执行结果

---- 0x10b1fd3d8 +[Animal load]
---- 0x10b1fd4c8 +[Person load]
---- 0x10b1fd478 +[Student load]
---- 0x10b1fd428 +[HighSchoolStudent load]
---- 0x10b1fd3d8 +[Animal(Test) load]
---- 0x10b1fd478 +[Student(Test2) load]
---- 0x10b1fd478 +[Student(Test1) load]
---- 0x10b1fd4c8 +[Person(Test1) load]
---- 0x10b1fd4c8 +[Person(Test2) load]

有了上面的经验,我们来看下现在的Complie Sources里面的顺序。

image

到现在为止我们能确定的是,所有分类的+load方法都要在所有类的+load方法之后执行。然后我们修改一些顺序。

image

再来看看执行结果。

---- 0x108ff1478 +[Person load]
---- 0x108ff14c8 +[Student load]
---- 0x108ff13d8 +[HighSchoolStudent load]
---- 0x108ff1428 +[Animal load]
---- 0x108ff1478 +[Person(Test2) load]
---- 0x108ff14c8 +[Student(Test2) load]
---- 0x108ff14c8 +[Student(Test1) load]
---- 0x108ff1478 +[Person(Test1) load]
---- 0x108ff1428 +[Animal(Test) load]

经过两次的对比我们发现,之前我们猜测正确:所有分类的+load方法都在所有类+load方法之后执行,同时又发现所有分类的+load方法的执行顺序与编译顺序有关,与是谁的分类无关,也与一个类有几个分类无关

接着上面咱们刚刚说的dyld的执行初始化方法继续说,在Runtime的源码中,可以看到call_load_methods方法的实现。

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;
}

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

  1. 循环调用call_class_loads方法,直到没有可执行的+load方法
  2. 调用call_category_loads方法
  3. 重复1->2,直到所有的类和分类的+load方法都执行完毕

所以在这里也能看出来,所有的类的+load方法都执行在分类的+load方法之前。

我们再来看看call_class_loads源码。

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

代码循环的次数是loadable_classes_used,这个变量在add_class_to_loadable_list方法中每添加一个Class对象,计数加一。所以在执行到这里的时候,就是当前所有已经加载好的Class对象的数量。loadable_classes数组也是在这个方法中一个一个把Class加进去的。所以无关的两个Class的执行顺序,与编译顺序有关。循环中得到load_method后,调用(*load_method)(cls, SEL_load)方法来调用+load方法。

接下来,再看一下call_category_loads方法

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // Compact detached list (order-preserving)
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[i];
        } else {
            shift++;
        }
    }
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[i];
    }

    // Destroy the new list.
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list. 
    // But if there's nothing left to load, destroy the list.
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }

    return new_categories_added;
}

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

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

我们都知道iOS应用的可执行文件,最后会作为一个镜像,加载到内存中,那如果我们还包含动态库和静态库呢?其实静态库会与我们的主程序编译在同一个可执行文件中,也就是一个镜像。但是即便他们在同一个镜像中,主程序与静态库都存在+load方法,其执行顺序是如何的呢?那与主程序不在同一镜像中的动态库中的+load方法,其执行顺序又是如何的呢?

我们在上面的Demo工程中,新建三个Target分别是Cocoa Touch Static Library,以及两个Cocoa Touch Framework,其中两个Framework,设定Mach-o Type一个是Static Library,一个是Dynamic Library
,Target名称分别为TestStaticLib、TestStaticFramework和TestDynamcFramework,三个Target中分别有一个类和对应的分类,的代码如下

@interface TestStaticLib : NSObject

@end

@implementation TestStaticLib

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end


@interface TestStaticLib (Test)

@end

@implementation TestStaticLib (Test)

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end
@interface TestStaticFramework : NSObject

@end

@implementation TestStaticFramework

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end


@interface TestStaticFramework (Test)

@end

@implementation TestStaticFramework (Test)

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end
@interface TestDynamicFramework : NSObject

@end

@implementation TestDynamicFramework

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end


@interface TestDynamicFramework (Test)

@end

@implementation TestDynamicFramework (Test)

+ (void)load
{
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end

执行观察结果

---- 0x1072ab1e8 +[TestDynamicFramework load]
---- 0x1072ab1e8 +[TestDynamicFramework(Test) load]
---- 0x106fb8478 +[Person load]
---- 0x106fb84c8 +[Student load]
---- 0x106fb83d8 +[HighSchoolStudent load]
---- 0x106fb8428 +[Animal load]
---- 0x106fb8478 +[Person(Test2) load]
---- 0x106fb84c8 +[Student(Test2) load]
---- 0x106fb84c8 +[Student(Test1) load]
---- 0x106fb8478 +[Person(Test1) load]
---- 0x106fb8428 +[Animal(Test) load]

首先输出的动态库中的类的+load方法及子类的+load方法,后面的是主工程的输出,我们之前已经看过的。

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

但是我们还发现一个问题,静态库.a.framework都没有打印结果。原因,我们也能想到,因为我们没有调用到这两个库的代码,所以也就没有把这两个库加载,链接进来。所以我们只需要在主工程代码中调用一下这两个库中的类即可。

#import "ViewController.h"
#import "TestStaticLib.h"
#import "TestStaticFramework.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [[TestStaticLib alloc] init];
    [[TestStaticFramework alloc] init];
}

@end

看下打印结果

---- 0x1060a1198 +[TestDynamicFramework load]
---- 0x1060a1198 +[TestDynamicFramework(Test) load]
---- 0x105dae518 +[Person load]
---- 0x105dae568 +[Student load]
---- 0x105dae478 +[HighSchoolStudent load]
---- 0x105dae4c8 +[Animal load]
---- 0x105dae5b8 +[TestStaticLib load]
---- 0x105dae608 +[TestStaticFramework load]
---- 0x105dae518 +[Person(Test2) load]
---- 0x105dae568 +[Student(Test2) load]
---- 0x105dae568 +[Student(Test1) load]
---- 0x105dae518 +[Person(Test1) load]
---- 0x105dae4c8 +[Animal(Test) load]

我们看到了连个静态库类的+load方法打印,在主程序的类的+load方法之后,在主程序的分类的+load方法之前。我们再在Buld Phases -> Link Binary With Libraries中修改一下两个静态库的先后顺序。

---- 0x1060a1198 +[TestDynamicFramework load]
---- 0x1060a1198 +[TestDynamicFramework(Test) load]
---- 0x105dae518 +[Person load]
---- 0x105dae568 +[Student load]
---- 0x105dae478 +[HighSchoolStudent load]
---- 0x105dae4c8 +[Animal load]
---- 0x105dae608 +[TestStaticFramework load]
---- 0x105dae5b8 +[TestStaticLib load]
---- 0x105dae518 +[Person(Test2) load]
---- 0x105dae568 +[Student(Test2) load]
---- 0x105dae568 +[Student(Test1) load]
---- 0x105dae518 +[Person(Test1) load]
---- 0x105dae4c8 +[Animal(Test) load]

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

但是这里还有一个问题,静态库中的分类的+load方法没有调用,其实经常使用静态库开发的同学就知道了,要在主工程的other linker flag中设置-all_load,设置完毕查看运行结果。

---- 0x108a68198 +[TestDynamicFramework load]
---- 0x108a68198 +[TestDynamicFramework(Test) load]
---- 0x108775608 +[Person load]
---- 0x108775658 +[Student load]
---- 0x108775568 +[HighSchoolStudent load]
---- 0x1087755b8 +[Animal load]
---- 0x1087756a8 +[TestStaticFramework load]
---- 0x1087756f8 +[TestStaticLib load]
---- 0x108775608 +[Person(Test2) load]
---- 0x108775658 +[Student(Test2) load]
---- 0x108775658 +[Student(Test1) load]
---- 0x108775608 +[Person(Test1) load]
---- 0x1087755b8 +[Animal(Test) load]
---- 0x1087756a8 +[TestStaticFramework(Test) load]
---- 0x1087756f8 +[TestStaticLib(Test) load]

看静态库中的分类的+load方法调用了,而且打印顺序与静态库中的类的+load方法的打印顺序一致。

如果在+load方法中调用[super load]会有什么影响

我们就继续看例子吧,还是在demo中Student的主类中的+load方法中,调用[super load]

@implementation Student

+ (void)load
{
    [super load];
    
    NSLog(@"---- %p %s", self, __FUNCTION__);
}

@end

查看打印结果

---- 0x10a7b4198 +[TestDynamicFramework load]
---- 0x10a7b4198 +[TestDynamicFramework(Test) load]
---- 0x10a4c1618 +[Person load]
---- 0x10a4c1668 +[Person(Test1) load]
---- 0x10a4c1668 +[Student load]
---- 0x10a4c1578 +[HighSchoolStudent load]
---- 0x10a4c15c8 +[Animal load]
---- 0x10a4c16b8 +[TestStaticFramework load]
---- 0x10a4c1708 +[TestStaticLib load]
---- 0x10a4c1618 +[Person(Test2) load]
---- 0x10a4c1668 +[Student(Test2) load]
---- 0x10a4c1668 +[Student(Test1) load]
---- 0x10a4c1618 +[Person(Test1) load]
---- 0x10a4c15c8 +[Animal(Test) load]
---- 0x10a4c16b8 +[TestStaticFramework(Test) load]
---- 0x10a4c1708 +[TestStaticLib(Test) load]

我们发现第四行调用了[Person(Test1) load]方法,而且在后面这个方法还继续调用了一次。这个原因是什么呢?

首先我们在之前得到的结论,在执行到Student+load方法之前,其父类Person+load方法已经完毕了。此时我们执行Student+load方法,调用了[super load],将父类的+load方法再次执行一次。那么这里为什么是[Person(Test1) load]呢,我们看一下编译顺序。

image

我们知道分类如果与类方法重名了,那么在之后调用时,会调用分类的同名方法,如果多个分类都实现了这个方法,那么就会按照编译顺序,最后执行最后编译的分类中的同名方法,于是就有了这样的结果。在后面,执行到分类的+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中的顺序)。
发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/86599216