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是类第一次接收到消息的时候调用,如果没有向这个类发送消息,则不会调用.

你可能感兴趣的:(load方法和initialize方法加载顺序)