美文网首页iOS底层探究i
深入了解+load方法的执行顺序

深入了解+load方法的执行顺序

作者: 再好一点点 | 来源:发表于2019-02-13 21:39 被阅读0次


    +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,查找入口点并返回

    首先新建一个Pserson类,实现+load方法并打断点,得到下图

    从图中可以看出执行顺序是_dyld_start->call_load_methods->Person(+load)

    由此可知+load方法会在dyld阶段的执行初始化方法中执行。

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

    一个类的+load方法调用在它的父类的+load方法之后

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

    接下来继续验证类与类之间的+load方法的执行顺序

    @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中看一下。

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

    ---- 0x1010331d8 +[Person load]

    ---- 0x101033138 +[Student load]

    ---- 0x1010330e8 +[HighSchoolStudent load]

    ---- 0x101033188 +[Animal load]

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

    查看打印结果

    ---- 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的两个分类Test1、Test2,以及Student的两分类Test1、Test2,和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里面的顺序。

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

    再来看看执行结果。

    ---- 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]呢,我们看一下编译顺序。

    我们知道分类如果与类方法重名了,那么在之后调用时,会调用分类的同名方法,如果多个分类都实现了这个方法,那么就会按照编译顺序,最后执行最后编译的分类中的同名方法,于是就有了这样的结果。在后面,执行到分类的+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 Sources中的顺序)

    6,同一镜像中所有的分类的+load方法的执行顺序与编译顺序有关(Compile Sources中的顺序),与是谁的分类,同一个类有几个分类无关

    7,同一镜像中主工程的+load方法执行在前,静态库的+load方法执行在后。有多个静态库时,静态库之间的执行顺序与编译顺序有关(Link Binary With Libraries中的顺序)

    8,不同镜像中,动态库的+load方法执行在前,主工程的+load执行在后,多个动态库的+load方法的执行顺序编译顺序有关(Link Binary With Libraries中的顺序)。

    相关文章

      网友评论

        本文标题:深入了解+load方法的执行顺序

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