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

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方法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容