美文网首页ios进阶
iOS runtime 源码分析 + load 和 + init

iOS runtime 源码分析 + load 和 + init

作者: 孙掌门 | 来源:发表于2019-12-19 17:28 被阅读0次

    iOS runtime 源码分析 + load 和 + initialize 原理讲解和总结

    load 源码分析

    runtime 源码从官网上面可以下载到,下面是我下载的objc4-756.2版本的runtime源码

    
    void prepare_load_methods(const headerType *mhdr)
    {
        size_t count, i;
    
        runtimeLock.assertLocked();
    
        classref_t *classlist = 
            _getObjc2NonlazyClassList(mhdr, &count);
        for (i = 0; i < count; i++) {
            schedule_class_load(remapClass(classlist[i]));
        }
    
        category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = categorylist[i];
            Class cls = remapClass(cat->cls);
            if (!cls) continue;  // category for ignored weak-linked class
            if (cls->isSwiftStable()) {
                _objc_fatal("Swift class extensions and categories on Swift "
                            "classes are not allowed to have +load methods");
            }
            realizeClassWithoutSwift(cls);
            assert(cls->ISA()->isRealized());
            add_category_to_loadable_list(cat);
        }
    }
    
    
    

    这些代码做了什么呢?首先是准备好被调用的类和分类,

    static void schedule_class_load(Class cls)
    {
        if (!cls) return;
        assert(cls->isRealized());  // _read_images should realize
    
        if (cls->data()->flags & RW_LOADED) return;
    
        // Ensure superclass-first ordering
        schedule_class_load(cls->superclass);
    
        add_class_to_loadable_list(cls);
        cls->setInfo(RW_LOADED); 
    }
    
    

    可以看到一个很关键的代码 schedule_class_load(cls->superclass); 又递归调用了,含义就是每次都回去查找父类,然后调用 add_class_to_loadable_list方法,将类添加到 loadable_classes 存储,接下来有调用

     category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = categorylist[i];
            Class cls = remapClass(cat->cls);
            if (!cls) continue;  // category for ignored weak-linked class
            if (cls->isSwiftStable()) {
                _objc_fatal("Swift class extensions and categories on Swift "
                            "classes are not allowed to have +load methods");
            }
            realizeClassWithoutSwift(cls);
            assert(cls->ISA()->isRealized());
            add_category_to_loadable_list(cat);
        }
    
    

    意思是加载所有的category,然后调用add_category_to_loadable_list,将category装到loadable_categories里面。接下来

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

    从这段代码中可以看出,是创建了一个autoreleasepool,然后先调用主类的方法 call_class_loads();,再调用 more_categories = call_category_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);
    }
    
    
    

    在这里我们可以看到是通过方法的首地址去调用的方法。

    +load 总结

    1.+ load 只要你动态加载或者静态引用了这个类,那么load方法就会执行,他不需要你去初始化才会执行,只会执行一次

    2.从上面的源码分析可以看出,是先调用父类中的load,然后调用分类中的load,顺序是,

    1.当前类父类load
    2.当前类load
    3.当前类分类load
    
    

    然后我们再main中和appdelegate中打断点,发现,刚执行到main,就已经打印了,所以执行顺序为

    
    1.当前类父类load
    2.当前类load
    3.当前类分类load
    4.main
    5.applegate
    
    

    说明一编译,load就加载了,

    2019-12-19 15:54:13.613320+0800 blogTest[96113:4739867] vc load
    2019-12-19 15:54:13.614405+0800 blogTest[96113:4739867] 父类load
    2019-12-19 15:54:13.614579+0800 blogTest[96113:4739867] 子类load
    2019-12-19 15:54:13.614855+0800 blogTest[96113:4739867] 分类load
    2019-12-19 15:54:13.615101+0800 blogTest[96113:4739867] url load
    (lldb) 
    
    

    我又加了一些打印,那么这些打印能否改变顺序呢?指的是没有继承关系的,平级的,答案是可以,既然取决于我们的编译,那么我们的工程配置中的 compile sources,不就是编译的文件吗?,我们试着拖一拖文件的顺序面试一下

    2019-12-19 15:58:28.754907+0800 blogTest[96242:4756770] vc load
    2019-12-19 15:58:28.755627+0800 blogTest[96242:4756770] 父类load
    2019-12-19 15:58:28.755734+0800 blogTest[96242:4756770] 子类load
    2019-12-19 15:58:28.755912+0800 blogTest[96242:4756770] url load
    2019-12-19 15:58:28.756158+0800 blogTest[96242:4756770] 分类load
    

    发现 url 的分类和我们测试的分类,顺序变了,是因为我拖动了他们的顺序,是不是很神奇?但是主类和分类的调用顺序是一定的,不取决于编译顺序

    load 总结和注意事项

    总结

    1. load 方法调用在main之前,并且不需要我们初始化,程序启动就会把所有文件加载
    2. 主类的调用优先于分类,分类的调动优先于当前类优先于分类
    3. 主类和分类的调用顺序跟编译顺序无关
    4. 分类之间加载,也就是平级之前加载取决于编译顺序,谁先编译就先加载谁

    注意事项

    1.我们发现。load 的加载比main 还要早,所以如果我们再load方法里面做了耗时的操作,那么一定会影响程序的启动时间,所以在load里面一定不要写耗时的代码。

    2.不要在load里面取加载对象,因为我们再load调用的时候根本就不确定我们的对象是否已经初始化了,所以不要去做对象的初始化

    调用顺序延伸(category)

    我之前的文章中讲过,分类中的同名方法,源码中是按照逆序加载的,也就是说后编译的分类方法会覆盖前面所有的同名的方法,分类还有一个特性就是,不管把声明写在主类还是分类,只要分类中实现了就可以找到,我们可以自己做测试

    + initialize

    源码太长,就先不放了

    initialize 方法会在类收到第一个消息时候调用,是一个懒加载的模式,如果一直没有收到消息,那么就一直不会调用,这样也是为了节省资源,从源码中我们可以看出来,当我们想对象发送消息的时候,如果没有初始化,会调用 _class_initialize,+initialize本质为objc_msgSend,如果子类没有实现initialize则会去父类查找,如果分类中实现,那么会覆盖主类,和runtime消息转发逻辑一样

    我的测试代码

    
    // 这个是父类
    @implementation Test
    +(void)load{
        NSLog(@"父类load:%@",[self class]);
    }
    +(void)initialize{
        NSLog(@"父类 initialize : %@",[self class]);
    }
    @end
    
    
    // 这个类继承 test,同时也初始化了另一个类
    
    @interface Forwarding : NSObject
    
    @end
    @implementation Forwarding
    - (void)print{
        NSLog(@"forwarding to print");
    }
    +(void)load{
        NSLog(@"forwarding load");
    }
    +(void)initialize{
        NSLog(@"forwarding initialize");
    }
    @end
    @implementation TestClass
    +(void)load{
        NSLog(@"子类load:%@",[self class]);
        
    }
    +(void)initialize{
        Forwarding *f = [[Forwarding alloc] init];
        [f print];
        NSLog(@"子类initialize");
    }
    
    

    然后我们看下打印结果,多余的不用管

    2019-12-19 17:10:31.260399+0800 blogTest[97767:4993277] vc load
    2019-12-19 17:10:31.260997+0800 blogTest[97767:4993277] forwarding load
    2019-12-19 17:10:31.261284+0800 blogTest[97767:4993277] 父类 initialize : Test
    2019-12-19 17:10:31.261383+0800 blogTest[97767:4993277] 父类load:Test
    2019-12-19 17:10:31.261473+0800 blogTest[97767:4993277] forwarding initialize
    2019-12-19 17:10:31.261543+0800 blogTest[97767:4993277] forwarding to print
    2019-12-19 17:10:31.261625+0800 blogTest[97767:4993277] 子类initialize
    2019-12-19 17:10:31.261735+0800 blogTest[97767:4993277] 子类load:TestClass
    2019-12-19 17:10:31.261808+0800 blogTest[97767:4993277] block load
    2019-12-19 17:10:31.261880+0800 blogTest[97767:4993277] url load
    2019-12-19 17:10:31.262189+0800 blogTest[97767:4993277] 分类load
    2019-12-19 17:10:31.262363+0800 blogTest[97767:4993277] 分类2load
    
    

    解释原因

    load 的打印顺序:

    我们只是把类加载到项目中,并没有写任何的代码,跑起来就有打印是为什么呢?,因为我们的程序一编辑,就会调用load方法,而load的调用顺序是先父类再当前类,所以肯定先打印父类load,然后子类load,最后分类load,可以从我们的打印中看到,这三个打印确实是这个顺序

    initialize 打印:

    因为先调用父类的 load,而我们在父类的load里面,调用了[self class],这行代码,其实就代表了,给当前类发消息了,前面说过,当第一次给这个类发消息的时候,就会调用 initialize,所以当我们在load里面写了[self class],之后就是发送了消息,就会调用initialize方法,所以可以看到我们的打印顺序为

    父类 initialize : Test
    父类load:Test
    

    ,然后接下来调用子类的load方法,同样在子类中也发送了消息,所以会调用子类的initialize,,在子类又给forwarding对象发送了消息,所以会滴啊用forwarding的initialize方法,然后调用initialize方法,紧接着回到子类的initialize,这样一整流程就结束了。

    那如果我们将代码改成这样呢,将子类的 initialize 去掉,打印结果为

    2019-12-19 17:23:13.313125+0800 blogTest[98099:5025080] 父类 initialize : Test
    2019-12-19 17:23:13.314034+0800 blogTest[98099:5025080] 父类load:Test
    2019-12-19 17:23:13.319165+0800 blogTest[98099:5025080] 父类 initialize : TestClass
    2019-12-19 17:23:13.319688+0800 blogTest[98099:5025080] 子类load:TestClass
    
    

    可以发现调用了两遍父类的 initialize,所以当发现子类中没有实现initialize方法之后,就去去父类查找,也证明了initialize方法会调用多次,这个流程就和我前面文章写得runtime消息转发流程一致,如果是这样我们猜想,如果分类中实现了initialize,会不会覆盖子类的?我们测试一下

    2019-12-19 17:26:45.895701+0800 blogTest[98211:5038894] 父类 initialize : Test
    2019-12-19 17:26:45.896573+0800 blogTest[98211:5038894] 父类load:Test
    2019-12-19 17:26:45.897588+0800 blogTest[98211:5038894] 分类中 initialize
    2019-12-19 17:26:45.898289+0800 blogTest[98211:5038894] 子类load:TestClass
    
    

    从打印结果可以发现,我们根本就没有走这个子类中的这个方法

    +(void)initialize{
        Forwarding *f = [[Forwarding alloc] init];
        [f print];
        NSLog(@"子类initialize");
    }
    
    

    所以这和我们的猜想是一样的,initialize 其实就是 objc_msgSend , 和消息转发流程是一样的,是不是觉得瞬间豁然开朗,如果没有,多读几遍,多测试几遍,你就明白了。

    总结:1.initialize 会在类第一次接收到消息的时候调用
    2.先调用父类的 initialize,然后调用子类。
    3. initialize 是通过 objc_msgSend 调用的
    4.如果子类没有实现 initialize,会调用父类的initialize(父类可能被调用多次)
    5.如果分类实现了initialize,会覆盖本类的initialize方法

    相关文章

      网友评论

        本文标题:iOS runtime 源码分析 + load 和 + init

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