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對象/關聯對象/多線程/內存管理/性能優化,相關圖片素材均取自課程中的課件。如有侵權,請聯系我刪除,謝謝!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容