load方法和initialize方法加载顺序

今天我们围绕2个问题研究laod方法的原理

  1. load方法什么时候调用的?
  2. load方法和initialize方法的区别是什么?他们在category中的调用顺序.
    在上一篇我们研究Category实现的原理二:分类信息如何添加到本类中已经知道,分类中的信息最后都会通过memmove,memcpy附加到本类中,并且会存放在本类信息的前面.所以分类和本类如果存在相同的方法,会优先调用分类中的方法.那么load方法也是如此吗?我们研究一下看看.
    首先创建一个Person类,和Person+Test1,Person+Test2两个分类,然后再创建一个Student类继承自Person类,以及Student的分类Student+Test1,重写他们的+ load方法,并添加一个 + test方法:
//Person
+ (void)load{
    NSLog(@"Person load");
}

+ (void)test{
    NSLog(@"Person test");
}
//Person+Test1 
+ (void)load{
    NSLog(@"Person+Test1 load");
}

+ (void)test{
    NSLog(@"Person+Test1 test");
}
//Person+Test2
+ (void)load{
    NSLog(@"Person+Test2 load");
}

+ (void)test{
    NSLog(@"Person+Test2 test");
}

//Student
+ (void)load{
    NSLog(@"Student load");
}
//Studeng+Test1
+ (void)load{
    NSLog(@"Student+Test1 load");
}

直接运行打印结果如下:

load方法打印结果

会发现我们连PersonStudent类的实例都没创建,每个类的load方法都打印了?这是为什么?难道分类中的load方法没有附加到本类中去吗?我们写个方法,打印出Person类中所有的方法查看一下:

- (void)getClassMethodWithClass:(Class)cls{
    unsigned int count;
    NSMutableString *names = [NSMutableString string];
    Method *methods = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i ++) {
        NSString *methodName = NSStringFromSelector(method_getName(methods[i]));
        [names appendFormat:@"%@ ,",methodName];
    }
    free(methods);
    NSLog(@"cls: %@, methods:%@",cls,names);
}

Person类中所有方法

从打印结果可以看出,load方法和test方法都已经附加到了本类中.

我们再调用一下Student+ Test方法看看调用结果:

test调用结果

从结果上可以看到test方法的确如我们之前所说,被附加到了本类中并且优先调用,那为什么每个类中load方法都会调用呢?
我们从runtime源码中寻找答案,查看源码步骤如下:
打开objc-os.mm文件->找到_objc_init()方法->进入load_images->进入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) {
            //先调用类的load方法
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        //再调用分类的load方法
        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;
}

可以从源码中看出,runtime会先调用类的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.
    //1: 遍历类列表
    for (i = 0; i < used; i++) {
        //2: 取出每一个类
        Class cls = classes[i].cls;
        //3: 取出每个类中的 load 方法
        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 方法
        (*load_method)(cls, SEL_load); //相当于 load(cls,sel)
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

其中第三步 取出每个类中的laod方法中的method是一个结构体,他是专门用来存储类中的load方法的:

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

分类的处理方法call_category_loads()和类的处理方法同理.所以我们现在明白了,为什么每个load方法都会调用,因为load方法是直接拿到每个类load方法的地址,直接调用,并不是像test()方法那样通过消息发送机制去查找.

总结一:load方法的加载顺序,是优先调用类的load方法,类的load方法调用完后,再调用分类中的load方法.

我们再思考一下,如果涉及到继承关系,load方法的调用顺序又是怎样的呢?
继续从runtime源码中找寻答案,再次进入load_images:

load_images

可以看到,load_images方法中,在执行call_load_methods()之前有个准备的过程prepare_load_methods,我们进入此方法:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, I;
    runtimeLock.assertWriting();

    //根据编译顺序把类存放到 classlist 中
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        //定制任务,规划任务,处理类的 load 方法.
        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
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        //把分类的load方法添加到 loadable_classes 列表中
        add_category_to_loadable_list(cat);
    }
}

刚才我们已经知道了,在call_class_loads内部遍历loadable_classes列表,遍历列表中类的load方法.实际上prepare_load_methods方法就是把类存放到loadable_classes列表中的.所以我们搞清楚prepare_load_methods方法内部存放类的顺序,就明白了load方法调用顺序.我们进入schedule_class_load:

schedule_class_load

从图中可以看到,我们传入一个类cls,在方法内部通过递归的形式找到cls的父类,然后再存储到loadalbe_classes中.所以,load方法的加载顺序是,优先调用父类的laod方法,再调用子类的
那如果没有子类关系,有很多同级别的类,laod方法是怎样调用的呢?我们可以试一下,新增CatDog类,然后运行:

可以看到,CatDog方法线运行,我们查看一下这些类的编译顺序:

会发现打印顺序和编译顺序是大致一致的,只是Student+Test1Student先编译,打印顺序却在Student后面,这不正是我们刚才所说的:优先加载类的laod方法,再加载分类的load方法嘛
ok,我们可以改变编译顺序,然后再运行:

结果:

会发现laod的调用顺序和编译顺序,完全一致.

我们用伪代码梳理一下load的加载步骤:

// 第一步: 把需要加载的 类 添加到 loadable_list 中
/*
classlist : 需要加载的类
count: 需要加载的类的数量
categorylist: 需要加载的分类
categoryCount: 需要加载的分类的数量
**/ 
// 1: 首先把类添加到 loadable_list 中
for (int I = 0 ; i < count ; i ++){
     class cls =  classlist[I];
     // 先找到 cls->superclass 添加到 loadable_list
    // 再把 cls 添加到 loadable_list
}

//2: 再把分类添加到 loadable_list 中
for (int I = 0 ; i < categoryCount ; i ++){
     class cls_cat =  categorylist[I];
    // 直接把 cls_cat 添加到 loadable_list
}

// 第二部: 调用 laod 方法
do {
    // 1: 先调用类的 load 方法

   // 2: 在调用分类的 load 方法
}while(...)

现在我们来一个大的总结:

  • load方法会在runtime加载类,分类的时候调用,即使你不使用这个类,同样也会调用.

  • 每个类的load方法在程序的运行过程中只会调用一次.

  • laod方法的调用顺序:
    一. 先调用类的+ load方法
    1:先按照编译顺序调用 (先编译,先调用)
    2:调用子类的+ load方法之前,会先调用父类的+ laod方法

    二. 在调用分类的+ laod方法
    1:按照编译先后顺序调用 (先编译,先调用)


initialize 方法

initialize方法和load方法很多人一直傻傻分不清楚,这两个方法的确也很容易搞混淆,下面我们将研究一下initialize方法,搞清楚他们之间的区别.
我们把上面讲解代码中的+ laod方法全部替换为+ initialize方法,然后运行代码,会发现控制台没有任何输出,initialize都没有调用.事实上,initialize方法是在向类第一次发送消息的时候调用的.
我们调用[Person alloc];然后再运行一下:


会发现调用了分类Person + Test1initialize方法,这说明initialize方法是通过msgSend(target,sel)消息发送来调用方法的.如果分类有相同的方法,会优先调用分类的方法.
那么含有继承关系的类调用initialize方法的顺序是怎样的呢?我们再创建一个Teacher类继承Person类,所以Person现在就有两个子类:Student,Teacher.
我们调用[Student alloc] , [Teacher alloc]方法然后运行:

会发现Person , Student , Teacher三个类的initialize方法都调用了,这是为什么呢?刚才我们说过,initialize方法是通过消息发送机制调用的,按理说它只会调用子类的中的方法,为什么父类的方法也会调用?
我们再把Student , Teacher中的initialize方法注释掉,再运行一下:

更加奇怪了,Person类的initialize调用了3次???
如果我们多调用几次[Student alloc] , [Teacher alloc]方法会怎样?
多次发送消息

现在我们可以大胆的猜测一下:initialize方法既然是通过msgSend(cls,sel)来实现的,而每次调用子类的initialize方法之前都会调用父类的initialize方法,从上图多次调用的运行结果我们也可以猜测,一个类的initialize方法只会调用一次,那么msgSend(cls,sel)方法在执行initialize方法之前很可能会判断父类的的是否已经初始化,如果父类没有初始化,则初始化父类.
那么到底是不是如我们猜测的那样呢?还是从runtime源码中找寻答案.
我们在runtime源码中搜索objc_msgSend(,会发现objc_msgSend()方法的源码都是汇编代码:
objc_msgSend()源码

汇编语言我也不懂,咱们可以换一个角度切入:消息发送机制的本质就是查找方法,调用实例方法就从类的方法列表中查找,调用类方法就从元类的方法列表中查找,我们在runtime源码中查找class_getClassMethod(:
class_getClassMethod 背部实现

原来class_getClassMethod的内部也是调用class_getInstanceMethod.点击 进入class_getInstanceMethod -> 进入lookUpImpOrNil -> 进入lookUpImpOrForward:
lookUpImpOrForward 方法

最后再进入lookUpImpOrForward会发现我们要找的重点:
重点部分

进入_class_initialize:

_class_initialize 方法

会发现正如我们猜测的一样:_class_initialize内部会先判断父类是否已经初始化,如果父类未初始化,则先初始化父类再初始化子类.

回到刚才的问题,为何Student , Teacher中的initialize方法都注释掉后,仍然打印3次initialize?
结合刚才的源码,我们可以大致分析一下[Student alloc] , [Teacher alloc]底层伪代码大致如下:

bool personIsInItializer = NO;
bool studentIsInItializer = NO;
bool teacherIsInItializer = NO;

//调用 [Student alloc]
if (Student 未被初始化){
         if (Student 的父类 Person 未被初始化) {
               1: 初始化 Person 类
               2: personIsInItializer = YES
     }
1: 初始化 Student 类
2: studentIsInItializer = YES;
}

// 调用 [Teacher alloc]
if (Teacher 未被初始化){
         if (Teacher 的父类 Person 未被初始化) {
               1: 初始化 Person 类
               2: personIsInItializer = YES
     }
1: 初始化 Teacher 类
2: teacherIsInItializer = YES;
}

伪代码执行结果

在如图的 第1,2,4步中都调用了person 的 initialize方法,所以打印了三遍Person initialize,但是要搞清楚,调用了三遍initialize方法并不表示Person类被初始化了3次,它还是初始化了一次,后面调用的两次的时候条件判断已经不成立了

initialize总结

  • initialize方法会在类第一次接收到消息的时候调用,先调用父类initialize,在调用子类的initialize.每个类只会初始化 1 次.
  • 如果子类没有实现initialize会调用父类的initialize,所以父类的initialize可能会被调用多次.
  • 如果分类实现了initialize,就覆盖了类本身的initialize

+ load+ initialize 方法的区别:

  • 调用方式:load是直接拿到函数地址,直接调用;initialize是通过消息机制调用.
  • 调用时机:loadruntime加载类或者分类的时候调用,不管有没有使用这个类,都会调用,也就是说load方法是肯定会执行的; 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