OC- +load 和 +initialize 方法调用原理

OC- +load+initialize 方法调用原理

image-20210419135423350
+load方法
+initialize方法
  1. load方法什么时候调用的?
  2. load方法和initialize方法的区别是什么?他们在category中的调用顺序.

Objective-C为我们提供了两种方法去运行对类进行相关设置的代码。

  • +load:该方法会在很早阶段(同时也是比较危险的阶段,可能导致崩溃)被调用,一旦某个类被Runtime加载,该类的+load方法就会被调用。我们可以在这个方法里面写一些必须要在程序运行非常早期阶段就需要运行的代码。
  • +initialize:该方法可以比较安全的处理大部分情况下的设置任务代码,因为会在一个更加安全的环境下被调用。你几乎可以在这个方法里面做任何事情,除非,你的代码需要等到外部实体向这个类发消息之后,才能运行,那么将你的代码放在+initialize方法里面将是不合适的

+load方法

load方法的调用时机和调用频率

首先创建一个Person类,和Person+Test1,Person+Test2两个分类,然后再创建一个Student类继承自Person类,以及Student的分类Student+Test1,重写他们的+ load方法,并添加一个 + test方法:

@interface MJPerson : NSObject
+ (void)test;
@end
    
#import "MJPerson.h"

@implementation MJPerson

+ (void)load
{
    NSLog(@"MJPerson +load");
}
+ (void)test
{
    NSLog(@"MJPerson +test");
}
@end

MJPerson+Test1

#import "MJPerson.h"

@interface MJPerson (Test1)

@end
#import "MJPerson+Test1.h"

@implementation MJPerson (Test1)

+ (void)load
{
    NSLog(@"MJPerson (Test1) +load");
}

+ (void)test
{
    NSLog(@"MJPerson (Test1) +test");
}

@end

MJPerson+Test2

#import "MJPerson.h"

@interface MJPerson (Test2)

@end
#import "MJPerson+Test2.h"

@implementation MJPerson (Test2)

+ (void)load
{
    NSLog(@"MJPerson (Test2) +load");
}

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

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }
    return 0;
}

RUN>

============打印输出============
2021-04-19 14:06:32.404129+0800 Interview01-load[2599:117222] MJPerson +load
2021-04-19 14:06:32.404641+0800 Interview01-load[2599:117222] MJPerson (Test1) +load
2021-04-19 14:06:32.404692+0800 Interview01-load[2599:117222] MJPerson (Test2) +load
image-20210419140729920

从日志看出,虽然整个工程都没有import过MJPerson以及它的两个分类,但是他们的load方法还是被调用了,并且都发生在main函数开始之前,而且+test并没有被调用。所以该现象间接证明了,load方法的调用应该和类对象以及分类的加载有关。

void printMethodNamesOfClass(Class cls)
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        printMethodNamesOfClass(object_getClass([MJPerson class]));
    }
    return 0;
}

RUN>

============打印输出============
2021-04-19 14:13:02.099579+0800 Interview01-load[2668:121765] MJPerson +load
2021-04-19 14:13:02.099997+0800 Interview01-load[2668:121765] MJPerson (Test1) +load
2021-04-19 14:13:02.100038+0800 Interview01-load[2668:121765] MJPerson (Test2) +load
2021-04-19 14:13:02.100215+0800 Interview01-load[2668:121765] MJPerson load, test, load, test, load, test,

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

image-20210419141524787

从结果上可以看到test方法的确如我们之前所说,被附加到了本类中并且优先调用,那为什么每个类中load方法都会调用呢?

接下来通过源码分析一下(Runtime源码下载地址)

我们从runtime源码中寻找答案,查看源码步骤如下:
打开objc-os.mm文件->找到_objc_init()方法->进入load_images->进入call_load_methods()方法

首先,进入Runtime的初始化文件objc-os.mm,找到_objc_init函数,该函数可以看作是Runtime的初始化函数。

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

直接看最后句_dyld_objc_notify_register(&map_images, load_images, unmap_image);其中很明显,load_images就是加载镜像/加载模块的意思,应该是与我们话题相关的参数,点进去看看它的实现

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

苹果对该函数官方给出的注释是,处理那些正在进行映射的镜像(images)的+load方法。该方法的实现里面,做了两件事情:

  • prepare_load_methods// Discover load methods -- 查找并准备load方法,以供后面去调用
  • call_load_methods();//Call +load methods -- 调用这些load方法

针对上面案例日志中出现的现象,先从结果出发,逆向分析,来看看load方法是如何调用的,进入call_load_methods();的实现

/***********************************************************************
* call_load_methods

* Call all pending class and category +load methods.
调用所有的处理中的class和category的+load方法;

* Class +load methods are called superclass-first. 
class的+load方法会被先调用,并且,一个调用一个class的+load方法前,会先对其父类的+load进行调用

* Category +load methods are not called until after the parent class's +load.
category的+load方法的调用,会发生在所有的class的+load方法完成调用之后。
* 
* This method must be RE-ENTRANT, because a +load could trigger 
* more image mapping. In addition, the superclass-first ordering 
* must be preserved in the face of re-entrant calls. Therefore, 
* only the OUTERMOST call of this function will do anything, and 
* that call will handle all loadable classes, even those generated 
* while it was running.
*
* The sequence below preserves +load ordering in the face of 
* image loading during a +load, and make sure that no 
* +load method is forgotten because it was added during 
* a +load call.

* Sequence:调用顺序
* 1. Repeatedly call class +loads until there aren't any more
遍历所有的class对象,调用它们的+load方法,知道所有class中的+load都完成了调用

* 2. Call category +loads ONCE.
调用所有category中的+load方法

* 3. Run more +loads if:
*    (a) there are more classes to load, OR
*    (b) there are some potential category +loads that have 
*        still never been attempted.
* Category +loads are only run once to ensure "parent class first" 
* ordering, even if a category +load triggers a new loadable class 
* and a new loadable category attached to that class. 
*
* Locking: loadMethodLock must be held by the caller 
*   All other locks must not be held.
**********************************************************************/
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();//先调用类的load方法
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();//再调用分类的load方法

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

很明显,核心逻辑在do-while循环里面,循环中面做了两件事:

  • 首先调用类对象的 +load方法--call_class_loads();,直到可加载的类的计数器减到0 --loadable_classes_used > 0
  • 然后调用分类的+load方法-- call_category_loads();//Call category +loads ONCE

我们进入call_class_loads()方法内部:这是对所有类对象(class)的+load方法的调用逻辑

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;//首先用局部变量loadable_class保存loadable_classes列表
    int used = loadable_classes_used;//在用局部变量used保存loadable_classes_used
    loadable_classes = nil;//将loadable_classes置空
    loadable_classes_allocated = 0;//将loadable_classes_allocated清零
    loadable_classes_used = 0;//将loadable_classes_used清零
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {//遍历classes列表
        Class cls = classes[i].cls;//从列表成员里面获得cls
        load_method_t load_method = (load_method_t)classes[i].method;//从列表成员获取对应cls的+load 的IMP(方法实现)
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);//这里就是对+load方法的调用,注意哦,这是直接的函数调用,不是消息机制那种哦,这里跟类的方法列表什么没关系,直接就是通过+load的IMP进行调用了
    }
    // Destroy the detached list.
    if (classes) free(classes);
}

上面实现的主要逻辑发生在for循环里面,该for循环遍历了一个叫classes的列表,该列表存储的是一堆loadable_class结构体,loadable_class的定义如下

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

每一个struct loadable_class变量,存储的应该就是 一个类对象 + 一个与该类相关的方法实现。从loadable_class这个命名,说明它内部的信息肯定是表示一个可以被加载的类的相关信息,因此合理推断,它里面的method应该就是类的+load方法,cls就是这个+load方法所对应的类对象。

我们再看看源码中对于classes这个数组进行遍历时到底做了什么。很简单,就是通过函数指针load_methodloadable_class中获得+load方法的IMP作为其参数,然后就直接对其进行调用(*load_method)(cls, SEL_load);,所以,<font color='red'>类对象的+load方法的调用实际上就发生在这里</font>。这里的for循环一旦结束,classes所包含的所有类对象的+load方法就会被依次调用,这跟一个类是否被在工程项目里被实例化过,是否接受过消息,没有关系。

至此,Runtime对于+load方法是如何调用的问题我们分析了一半,弄清楚了类对象的+load方法的是怎么被一个一个调用的,也就是static void call_class_loads(void)这个函数,接下来,还有问题的另一半--static bool call_category_loads(void),也就是关于分类的+load方法的调用。进入其中

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.--------->A 分离可加载categor`列表
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list. -------->B 调用detached list 里面的所有+load方法
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // Compact detached list (order-preserving) --------->C 清理cats 里面已经被消费过的成员,并且更新used计数值
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[i];
        } else {
            shift++;
        }
    }
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list. ---->D 如果又出现了新的可加载的分类,将其相关内容复制到`cats`列表上
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[i];
    }

    // Destroy the new list.------->E  销毁列表 loadable_categories
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list. 
    // But if there's nothing left to load, destroy the list.
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }
    return new_categories_added;
}

我们可以看到,这个方法的实现里面,通过系统注释,被划分如下几块:

  • A -- // Detach current loadable list.分离可加载category列表,也就是把可加载列表的信息保存到本函数的局部变量cats数组上。
  • B -- // Call all +loads for the detached list.消费cats里面的所有+load方法(也就是调用它们)
  • C -- // Compact detached list (order-preserving)清理cats里面已经被消费过的成员,并且更新used计数值
  • D -- // Copy any new +load candidates from the new list to the detached list.如果又出现了新的可加载的分类,将其相关内容复制到cats列表上。
  • E -- // Destroy the new list.销毁列表(这里指的是外部的loadable_categories变量)
  • F -- // Reattach the (now augmented) detached list. But if there's nothing left to load, destroy the list.更新几个记录了category+load信息的几个全局变量。

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

小结 Runtime对于+load方法的调用,不是走的我们熟悉的“消息发送”路线,而是直接拿到+load方法的IMP,直接调用。因此不存在所谓“类的方法被category的方法覆盖”的问题.

目前,我们确定了<font color=FF0000>类对象的+load方法会先于分类的+load方法被调用</font>,并且不存在覆盖现象。

  • 那么对于类于类之间+load调用顺序是怎样的?
  • 同样的疑问对于分类(category)又是如何呢?
    这两个问题,我们就需要进入prepare_load_methods方法的实现,看看+load方法被调用前,Runtime是如何准备它们的。
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();
    //根据编译顺序把类存放到 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);
    }
}

classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count);可以看出,利用系统提供的函数_getObjc2NonlazyClassList,获得类对象的列表,因为这是系统级别的函数,应该跟编译过程的顺序有关,这里先推测classlist中类的顺序与类的编译顺序相同。

接下来,就是遍历classlist,对其每个成员通过函数schedule_class_load()进行处理

/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
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);//递归查找,优先调用父类。

    /**
     将cls添加到loadable_classes数组的最后面
     */
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
  • 先递归调用自身(schedule_class_load()),对当前类(也就是函数传入的参数)的父类进行处理
  • 处理完父类之后,将当前类对象加入到可加载类的相关列表当中 add_class_to_loadable_list(cls);

<font color=FF0000>经过这样的整理之后,最终整理过的装载类对象相关信息的数组中,父类应该排在子类前面。而不同的类对象之间在数组中的位置,就可以参考它们.m的编译顺序来看了</font>,load方法的加载顺序是,优先调用父类的laod方法,再调用子类的laod方法

那如果没有子类关系,有很多同级别的类,laod方法是怎样调用的呢?我们可以试一下,新增CatDog类,然后运行:

image-20210419145514162

可以看到,CatDog方法线运行,打印顺序和编译顺序是大致一致的

image-20210419145751492

优先加载类的laod方法,再加载分类的load方法嘛

小结

  • 那么对于类于类之间+load调用顺序是怎样的?
    调用一个类对象的+load方法之前,会先调用其父类的+load方法(如果存在的话),类与类之间,会按照编译的顺序,先后调用其+load方法。一个类对象的+load方法不会被重复调用,只可能被调用一次。
  • 同样的疑问对于分类(category)又是如何呢?
    分类的+load方法,会按照分类参与编译的顺序,先编译的,先被调用。

+initialize 方法

initialize方法和load方法很多人一直傻傻分不清楚,这两个方法的确也很容易搞混淆,下面我们将研究一下initialize方法,搞清楚他们之间的区别.

关于+initialize方法的一些结论
  • +initialize方法会在类第一次接收到消息的时候调用
  • +initialize方法是通过objc_msgSend()进行调用的
#import "MJPerson.h"

@implementation MJPerson

+ (void)initialize
{
    NSLog(@"MJPerson +initialize");
}

@end
#import "MJPerson+Test1.h"

@implementation MJPerson (Test1)

+ (void)initialize
{
    NSLog(@"MJPerson (Test1) +initialize");
}
@end
#import "MJPerson+Test2.h"

@implementation MJPerson (Test2)

+ (void)initialize
{
    NSLog(@"MJPerson (Test2) +initialize");
}

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
         }
    return 0;
}

RUN>

没有任何输出

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [MJPerson alloc];
    }
    return 0;
}

RUN

2021-04-19 15:19:39.887208+0800 Interview01-load[3169:158165] MJPerson (Test2) +initialize
image-20210419153421487

会发现调用了分类Person + Test2initialize方法,这说明initialize方法是通过msgSend(target,sel)消息发送来调用方法的.如果分类有相同的方法,会优先调用分类的方法.

那么含有继承关系的类调用initialize方法的顺序是怎样的呢?我们再创建一个Teacher类继承Person类,所以Person现在就有两个子类:Student,Teacher.

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [MJStudent alloc];
        [MJTeacher alloc];
    }
    return 0;
}

RUN>

2021-04-19 15:40:11.670509+0800 Interview01-load[3315:167613] MJPerson (Test2) +initialize
2021-04-19 15:40:11.671034+0800 Interview01-load[3315:167613] MJStudent +initialize
2021-04-19 15:40:11.671094+0800 Interview01-load[3315:167613] MJTeacher +initialize

会发现Person , Student , Teacher三个类的initialize方法都调用了,这是为什么呢?刚才我们说过,initialize方法是通过消息发送机制调用的,<font color=FF0000>按理说它只会调用子类的中的方法,为什么父类的方法也会调用?</font>

我们再把Student , Teacher中的initialize方法注释掉,再运行一下:

2021-04-19 15:44:36.106119+0800 Interview01-load[3343:169965] MJPerson (Test2) +initialize
2021-04-19 15:44:36.106554+0800 Interview01-load[3343:169965] MJPerson (Test2) +initialize
2021-04-19 15:44:36.106601+0800 Interview01-load[3343:169965] MJPerson (Test2) +initialize
image-20210419154812624

从上图多次调用的运行结果我们也可以猜测,一个类的initialize方法只会调用一次,那么msgSend(cls,sel)方法内在执行initialize方法之前很可能会判断父类的的是否已经初始化,如果父类没有初始化,则初始化父类.

那么到底是不是如我们猜测的那样呢?还是从runtime源码中找寻答案.
我们在runtime源码中搜索objc_msgSend(,会发现objc_msgSend()方法的源码都是汇编代码:

image-20210419155402491

后面流程略,从MJ老师课程直接进入objc_msgLookup等价的方法,它就是Method class_getInstanceMethod(Class cls, SEL sel)。进入该方法查看一下lookUpImpOrNil -> 进入lookUpImpOrForward:

/***********************************************************************
* lookUpImpOrNil.
* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache
**********************************************************************/
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

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

......
 //initialize是否需要初始化   !cls->isInitialized这个类没有初始化
 if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
......

上面会判断如果需要初始化并且这个类没有初始化,就进入_class_initialize方法进行初始化,验证了,一个类只初始化一次。

进入_class_initialize:

void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());
 
    Class supercls;
    bool reallyInitialize = NO;

    //如果有父类,并且父类没有初始化就递归调用,初始化父类
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
......
    //没有父类或者父类已经初始化,开始初始化子类
    callInitialize(cls); //初始化子类
......

上面会先判断如果有父类并且父类没有初始化就递归调用,初始化父类,如果没有父类或者父类已经初始化,就开始初始化子类。验证了,先初始化父类,再初始化子类。
进入callInitialize, 开始初始化这个类

void callInitialize(Class cls)
{
    //第一个参数是类,第二个参数是SEL_initialize消息
    //就是给某个类发送SEL_initialize消息
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

通过上面的源码分析,可以知道,的确是先调用父类的Initialize再调用子类的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;
}
image-20210419161020057

initialize总结

+initialize方法会在类对象 *第一次* 接收到消息的时候调用

调用顺序:调用某个类的+initialize之前,会先调用其父类的+initialize(前提是父类的+initialize从来没有被调用过)

由于+initialize的调用,是通过消息机制,也就是objc_msgSend(),因此如果子类的+initialize没有实现,就会去调用父类的+initialize

基于同样的原因,如果分类实现的+initialize,那么就会“覆盖”类对象本身的+initialize方法而被调用。

+ load+ initialize 方法的区别:

  • 调用方式:load是直接拿到函数地址,直接调用;initialize是通过消息机制调用.
  • 调用时机:loadruntime加载类或者分类的时候调用,不管有没有使用这个类,都会调用,也就是<font color=FF0000>说load方法是肯定会执行的</font>; initialize是类第一次接收到消息的时候调用,如果没有向这个类发送消息,则不会调用.

面试题:

问题一:+load方法和+ Initialize方法的区别是什么?

  1. 调用时机:load是在Runtime加载类、分类的时候调用,只会调用一次,Initialize是在类第一次接收到消息时调用,每一个类只会初始化一次。
  2. 调用方式:load是根据函数地址直接调用,Initialize是通过objc_msgSend调用。

问题二:说一下load和Initialize的调用顺序?

对于load:先调用父类的+load,后调用子类的+load,再调用分类的+load,并且先编译的先调用

对于Initialize:先调用父类的+initialize,再调用子类的+initialize(先初始化父类,再初始化子类)

特别备注

本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

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

推荐阅读更多精彩内容