ios进阶

iOS runtime 源码分析 + load 和 + init

2019-12-19  本文已影响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方法

上一篇下一篇

猜你喜欢

热点阅读