OC- +load 和 +initialize 方法調(diào)用原理

OC- +load+initialize 方法調(diào)用原理

image-20210419135423350
+load方法
+initialize方法
  1. load方法什么時(shí)候調(diào)用的?
  2. load方法和initialize方法的區(qū)別是什么?他們?cè)?code>category中的調(diào)用順序.

Objective-C為我們提供了兩種方法去運(yùn)行對(duì)類進(jìn)行相關(guān)設(shè)置的代碼账胧。

  • +load:該方法會(huì)在很早階段(同時(shí)也是比較危險(xiǎn)的階段携取,可能導(dǎo)致崩潰)被調(diào)用,一旦某個(gè)類被Runtime加載处坪,該類的+load方法就會(huì)被調(diào)用。我們可以在這個(gè)方法里面寫一些必須要在程序運(yùn)行非常早期階段就需要運(yùn)行的代碼丝蹭。
  • +initialize:該方法可以比較安全的處理大部分情況下的設(shè)置任務(wù)代碼秤掌,因?yàn)闀?huì)在一個(gè)更加安全的環(huán)境下被調(diào)用。你幾乎可以在這個(gè)方法里面做任何事情再来,除非蒙兰,你的代碼需要等到外部實(shí)體向這個(gè)類發(fā)消息之后磷瘤,才能運(yùn)行,那么將你的代碼放在+initialize方法里面將是不合適的

+load方法

load方法的調(diào)用時(shí)機(jī)和調(diào)用頻率

首先創(chuàng)建一個(gè)Person類,和Person+Test1,Person+Test2兩個(gè)分類,然后再創(chuàng)建一個(gè)Student類繼承自Person類,以及Student的分類Student+Test1,重寫他們的+ load方法,并添加一個(gè) + 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

從日志看出搜变,雖然整個(gè)工程都沒有import過MJPerson以及它的兩個(gè)分類采缚,但是他們的load方法還是被調(diào)用了,并且都發(fā)生在main函數(shù)開始之前挠他,而且+test并沒有被調(diào)用扳抽。所以該現(xiàn)象間接證明了,load方法的調(diào)用應(yīng)該和類對(duì)象以及分類的加載有關(guān)殖侵。

void printMethodNamesOfClass(Class cls)
{
    unsigned int count;
    // 獲得方法數(shù)組
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存儲(chǔ)方法名
    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,

從打印結(jié)果可以看出,load方法和test方法都已經(jīng)附加到了本類中.

image-20210419141524787

從結(jié)果上可以看到test方法的確如我們之前所說,被附加到了本類中并且優(yōu)先調(diào)用,那為什么每個(gè)類中load方法都會(huì)調(diào)用呢?

接下來通過源碼分析一下(Runtime源碼下載地址)

我們從runtime源碼中尋找答案,查看源碼步驟如下:
打開objc-os.mm文件->找到_objc_init()方法->進(jìn)入load_images->進(jìn)入call_load_methods()方法

首先贸呢,進(jìn)入Runtime的初始化文件objc-os.mm,找到_objc_init函數(shù)愉耙,該函數(shù)可以看作是Runtime的初始化函數(shù)贮尉。

/***********************************************************************
* _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就是加載鏡像/加載模塊的意思朴沿,應(yīng)該是與我們?cè)掝}相關(guān)的參數(shù)猜谚,點(diǎn)進(jìn)去看看它的實(shí)現(xiàn)

/***********************************************************************
* 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();
}

蘋果對(duì)該函數(shù)官方給出的注釋是,處理那些正在進(jìn)行映射的鏡像(images)的+load方法赌渣。該方法的實(shí)現(xiàn)里面魏铅,做了兩件事情:

  • prepare_load_methods// Discover load methods -- 查找并準(zhǔn)備load方法,以供后面去調(diào)用
  • call_load_methods();//Call +load methods -- 調(diào)用這些load方法

針對(duì)上面案例日志中出現(xiàn)的現(xiàn)象坚芜,先從結(jié)果出發(fā)览芳,逆向分析,來看看load方法是如何調(diào)用的鸿竖,進(jìn)入call_load_methods();的實(shí)現(xiàn)

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

* Call all pending class and category +load methods.
調(diào)用所有的處理中的class和category的+load方法沧竟;

* Class +load methods are called superclass-first. 
class的+load方法會(huì)被先調(diào)用,并且缚忧,一個(gè)調(diào)用一個(gè)class的+load方法前悟泵,會(huì)先對(duì)其父類的+load進(jìn)行調(diào)用

* Category +load methods are not called until after the parent class's +load.
category的+load方法的調(diào)用,會(huì)發(fā)生在所有的class的+load方法完成調(diào)用之后闪水。
* 
* 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:調(diào)用順序
* 1. Repeatedly call class +loads until there aren't any more
遍歷所有的class對(duì)象糕非,調(diào)用它們的+load方法,知道所有class中的+load都完成了調(diào)用

* 2. Call category +loads ONCE.
調(diào)用所有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();//先調(diào)用類的load方法
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();//再調(diào)用分類的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循環(huán)里面朽肥,循環(huán)中面做了兩件事:

  • 首先調(diào)用類對(duì)象的 +load方法--call_class_loads();,直到可加載的類的計(jì)數(shù)器減到0 --loadable_classes_used > 0
  • 然后調(diào)用分類的+load方法-- call_category_loads();//Call category +loads ONCE

我們進(jìn)入call_class_loads()方法內(nèi)部:這是對(duì)所有類對(duì)象(class)的+load方法的調(diào)用邏輯

/***********************************************************************
* 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;//從列表成員獲取對(duì)應(yīng)cls的+load 的IMP(方法實(shí)現(xiàn))
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);//這里就是對(duì)+load方法的調(diào)用持钉,注意哦衡招,這是直接的函數(shù)調(diào)用,不是消息機(jī)制那種哦每强,這里跟類的方法列表什么沒關(guān)系蚁吝,直接就是通過+load的IMP進(jìn)行調(diào)用了
    }
    // Destroy the detached list.
    if (classes) free(classes);
}

上面實(shí)現(xiàn)的主要邏輯發(fā)生在for循環(huán)里面旱爆,該for循環(huán)遍歷了一個(gè)叫classes的列表,該列表存儲(chǔ)的是一堆loadable_class結(jié)構(gòu)體窘茁,loadable_class的定義如下

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

每一個(gè)struct loadable_class變量怀伦,存儲(chǔ)的應(yīng)該就是 一個(gè)類對(duì)象 + 一個(gè)與該類相關(guān)的方法實(shí)現(xiàn)。從loadable_class這個(gè)命名山林,說明它內(nèi)部的信息肯定是表示一個(gè)可以被加載的類的相關(guān)信息房待,因此合理推斷,它里面的method應(yīng)該就是類的+load方法驼抹,cls就是這個(gè)+load方法所對(duì)應(yīng)的類對(duì)象桑孩。

我們?cè)倏纯丛创a中對(duì)于classes這個(gè)數(shù)組進(jìn)行遍歷時(shí)到底做了什么。很簡(jiǎn)單框冀,就是通過函數(shù)指針load_methodloadable_class中獲得+load方法的IMP作為其參數(shù)流椒,然后就直接對(duì)其進(jìn)行調(diào)用(*load_method)(cls, SEL_load);,所以明也,<font color='red'>類對(duì)象的+load方法的調(diào)用實(shí)際上就發(fā)生在這里</font>宣虾。這里的for循環(huán)一旦結(jié)束,classes所包含的所有類對(duì)象的+load方法就會(huì)被依次調(diào)用温数,這跟一個(gè)類是否被在工程項(xiàng)目里被實(shí)例化過绣硝,是否接受過消息,沒有關(guān)系撑刺。

至此鹉胖,Runtime對(duì)于+load方法是如何調(diào)用的問題我們分析了一半,弄清楚了類對(duì)象的+load方法的是怎么被一個(gè)一個(gè)調(diào)用的够傍,也就是static void call_class_loads(void)這個(gè)函數(shù)甫菠,接下來,還有問題的另一半--static bool call_category_loads(void)冕屯,也就是關(guān)于分類的+load方法的調(diào)用淑蔚。進(jìn)入其中

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 調(diào)用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 里面已經(jīng)被消費(fèi)過的成員,并且更新used計(jì)數(shù)值
    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 如果又出現(xiàn)了新的可加載的分類愕撰,將其相關(guān)內(nèi)容復(fù)制到`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;
}

我們可以看到,這個(gè)方法的實(shí)現(xiàn)里面醋寝,通過系統(tǒng)注釋搞挣,被劃分如下幾塊:

  • A -- // Detach current loadable list.分離可加載category列表,也就是把可加載列表的信息保存到本函數(shù)的局部變量cats數(shù)組上音羞。
  • B -- // Call all +loads for the detached list.消費(fèi)cats里面的所有+load方法(也就是調(diào)用它們)
  • C -- // Compact detached list (order-preserving)清理cats里面已經(jīng)被消費(fèi)過的成員囱桨,并且更新used計(jì)數(shù)值
  • D -- // Copy any new +load candidates from the new list to the detached list.如果又出現(xiàn)了新的可加載的分類,將其相關(guān)內(nèi)容復(fù)制到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.更新幾個(gè)記錄了category+load信息的幾個(gè)全局變量舍肠。

分類的處理方法call_category_loads()和類的處理方法同理.所以我們現(xiàn)在明白了,為什么每個(gè)load方法都會(huì)調(diào)用,<font color=FF0000>因?yàn)?code>load方法是直接拿到每個(gè)類load方法的地址,直接調(diào)用,并不是像test()方法那樣通過消息發(fā)送機(jī)制去查找.</font>

小結(jié) Runtime對(duì)于+load方法的調(diào)用搀继,不是走的我們熟悉的“消息發(fā)送”路線,而是直接拿到+load方法的IMP,直接調(diào)用。因此不存在所謂“類的方法被category的方法覆蓋”的問題.

目前泻蚊,我們確定了<font color=FF0000>類對(duì)象的+load方法會(huì)先于分類的+load方法被調(diào)用</font>轧抗,并且不存在覆蓋現(xiàn)象。

  • 那么對(duì)于類于類之間+load調(diào)用順序是怎樣的刨肃?
  • 同樣的疑問對(duì)于分類(category)又是如何呢?
    這兩個(gè)問題,我們就需要進(jìn)入prepare_load_methods方法的實(shí)現(xiàn)黑滴,看看+load方法被調(diào)用前,Runtime是如何準(zhǔn)備它們的紧索。
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();
    //根據(jù)編譯順序把類存放到 classlist 中
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        //定制任務(wù),規(guī)劃任務(wù),處理類的 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);可以看出袁辈,利用系統(tǒng)提供的函數(shù)_getObjc2NonlazyClassList,獲得類對(duì)象的列表珠漂,因?yàn)檫@是系統(tǒng)級(jí)別的函數(shù)晚缩,應(yīng)該跟編譯過程的順序有關(guān),這里先推測(cè)classlist中類的順序與類的編譯順序相同甘磨。

接下來橡羞,就是遍歷classlist,對(duì)其每個(gè)成員通過函數(shù)schedule_class_load()進(jìn)行處理

/***********************************************************************
* 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);//遞歸查找济舆,優(yōu)先調(diào)用父類卿泽。

    /**
     將cls添加到loadable_classes數(shù)組的最后面
     */
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
  • 先遞歸調(diào)用自身(schedule_class_load()),對(duì)當(dāng)前類(也就是函數(shù)傳入的參數(shù))的父類進(jìn)行處理
  • 處理完父類之后滋觉,將當(dāng)前類對(duì)象加入到可加載類的相關(guān)列表當(dāng)中 add_class_to_loadable_list(cls);

<font color=FF0000>經(jīng)過這樣的整理之后签夭,最終整理過的裝載類對(duì)象相關(guān)信息的數(shù)組中,父類應(yīng)該排在子類前面椎侠。而不同的類對(duì)象之間在數(shù)組中的位置第租,就可以參考它們.m的編譯順序來看了</font>,load方法的加載順序是,優(yōu)先調(diào)用父類的laod方法,再調(diào)用子類的laod方法

那如果沒有子類關(guān)系,有很多同級(jí)別的類,laod方法是怎樣調(diào)用的呢?我們可以試一下,新增CatDog類,然后運(yùn)行:

image-20210419145514162

可以看到,CatDog方法線運(yùn)行,打印順序和編譯順序是大致一致的

image-20210419145751492

優(yōu)先加載類的laod方法,再加載分類的load方法嘛

小結(jié)

  • 那么對(duì)于類于類之間+load調(diào)用順序是怎樣的?
    調(diào)用一個(gè)類對(duì)象的+load方法之前我纪,會(huì)先調(diào)用其父類的+load方法(如果存在的話)慎宾,類與類之間,會(huì)按照編譯的順序浅悉,先后調(diào)用其+load方法趟据。一個(gè)類對(duì)象的+load方法不會(huì)被重復(fù)調(diào)用,只可能被調(diào)用一次术健。
  • 同樣的疑問對(duì)于分類(category)又是如何呢汹碱?
    分類的+load方法,會(huì)按照分類參與編譯的順序荞估,先編譯的咳促,先被調(diào)用稚新。

+initialize 方法

initialize方法和load方法很多人一直傻傻分不清楚,這兩個(gè)方法的確也很容易搞混淆,下面我們將研究一下initialize方法,搞清楚他們之間的區(qū)別.

關(guān)于+initialize方法的一些結(jié)論
  • +initialize方法會(huì)在類第一次接收到消息的時(shí)候調(diào)用
  • +initialize方法是通過objc_msgSend()進(jìn)行調(diào)用的
#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

會(huì)發(fā)現(xiàn)調(diào)用了分類Person + Test2initialize方法,這說明initialize方法是通過msgSend(target,sel)消息發(fā)送來調(diào)用方法的.如果分類有相同的方法,會(huì)優(yōu)先調(diào)用分類的方法.

那么含有繼承關(guān)系的類調(diào)用initialize方法的順序是怎樣的呢?我們?cè)賱?chuàng)建一個(gè)Teacher類繼承Person類,所以Person現(xiàn)在就有兩個(gè)子類: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

會(huì)發(fā)現(xiàn)Person , Student , Teacher三個(gè)類的initialize方法都調(diào)用了,這是為什么呢?剛才我們說過,initialize方法是通過消息發(fā)送機(jī)制調(diào)用的,<font color=FF0000>按理說它只會(huì)調(diào)用子類的中的方法,為什么父類的方法也會(huì)調(diào)用?</font>

我們?cè)侔?code>Student , Teacher中的initialize方法注釋掉,再運(yùn)行一下:

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

從上圖多次調(diào)用的運(yùn)行結(jié)果我們也可以猜測(cè),一個(gè)類的initialize方法只會(huì)調(diào)用一次,那么msgSend(cls,sel)方法內(nèi)在執(zhí)行initialize方法之前很可能會(huì)判斷父類的的是否已經(jīng)初始化,如果父類沒有初始化,則初始化父類.

那么到底是不是如我們猜測(cè)的那樣呢?還是從runtime源碼中找尋答案.
我們?cè)?code>runtime源碼中搜索objc_msgSend(,會(huì)發(fā)現(xiàn)objc_msgSend()方法的源碼都是匯編代碼:

image-20210419155402491

后面流程略,從MJ老師課程直接進(jìn)入objc_msgLookup等價(jià)的方法跪腹,它就是Method class_getInstanceMethod(Class cls, SEL sel)褂删。進(jìn)入該方法查看一下lookUpImpOrNil -> 進(jìn)入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;
}

最后再進(jìn)入lookUpImpOrForward會(huì)發(fā)現(xiàn)我們要找的重點(diǎn):

......
 //initialize是否需要初始化   !cls->isInitialized這個(gè)類沒有初始化
 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
    }
......

上面會(huì)判斷如果需要初始化并且這個(gè)類沒有初始化,就進(jìn)入_class_initialize方法進(jìn)行初始化尺迂,驗(yàn)證了笤妙,一個(gè)類只初始化一次。

進(jìn)入_class_initialize:

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

    //如果有父類噪裕,并且父類沒有初始化就遞歸調(diào)用蹲盘,初始化父類
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
......
    //沒有父類或者父類已經(jīng)初始化,開始初始化子類
    callInitialize(cls); //初始化子類
......

上面會(huì)先判斷如果有父類并且父類沒有初始化就遞歸調(diào)用膳音,初始化父類召衔,如果沒有父類或者父類已經(jīng)初始化,就開始初始化子類祭陷。驗(yàn)證了苍凛,先初始化父類,再初始化子類兵志。
進(jìn)入callInitialize醇蝴, 開始初始化這個(gè)類

void callInitialize(Class cls)
{
    //第一個(gè)參數(shù)是類,第二個(gè)參數(shù)是SEL_initialize消息
    //就是給某個(gè)類發(fā)送SEL_initialize消息
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

通過上面的源碼分析想罕,可以知道悠栓,的確是先調(diào)用父類的Initialize再調(diào)用子類的Initialize,并且一個(gè)類只會(huì)初始化一次按价。

經(jīng)過一系列的調(diào)用過程惭适,會(huì)發(fā)現(xiàn)正如我們猜測(cè)的一樣:_class_initialize內(nèi)部會(huì)先判斷父類是否已經(jīng)初始化,如果父類未初始化,則先初始化父類再初始化子類.

回到剛才的問題,為何Student , Teacher中的initialize方法都注釋掉后,仍然打印3次initialize?

結(jié)合剛才的源碼,我們可以大致分析一下[Student alloc] , [Teacher alloc]底層偽代碼大致如下:

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

//調(diào)用 [Student alloc]
if (Student 未被初始化){
         if (Student 的父類 Person 未被初始化) {
               1: 初始化 Person 類
               2: personIsInItializer = YES
     }
1: 初始化 Student 類
2: studentIsInItializer = YES;
}

// 調(diào)用 [Teacher alloc]
if (Teacher 未被初始化){
         if (Teacher 的父類 Person 未被初始化) {
               1: 初始化 Person 類
               2: personIsInItializer = YES
     }
1: 初始化 Teacher 類
2: teacherIsInItializer = YES;
}
image-20210419161020057

initialize總結(jié)

+initialize方法會(huì)在類對(duì)象 *第一次* 接收到消息的時(shí)候調(diào)用

調(diào)用順序:調(diào)用某個(gè)類的+initialize之前,會(huì)先調(diào)用其父類的+initialize(前提是父類的+initialize從來沒有被調(diào)用過)

由于+initialize的調(diào)用楼镐,是通過消息機(jī)制癞志,也就是objc_msgSend(),因此如果子類的+initialize沒有實(shí)現(xiàn)框产,就會(huì)去調(diào)用父類的+initialize

基于同樣的原因凄杯,如果分類實(shí)現(xiàn)的+initialize,那么就會(huì)“覆蓋”類對(duì)象本身的+initialize方法而被調(diào)用秉宿。

+ load+ initialize 方法的區(qū)別:

  • 調(diào)用方式:load是直接拿到函數(shù)地址,直接調(diào)用;initialize是通過消息機(jī)制調(diào)用.
  • 調(diào)用時(shí)機(jī):loadruntime加載類或者分類的時(shí)候調(diào)用,不管有沒有使用這個(gè)類,都會(huì)調(diào)用,也就是<font color=FF0000>說load方法是肯定會(huì)執(zhí)行的</font>; initialize是類第一次接收到消息的時(shí)候調(diào)用,如果沒有向這個(gè)類發(fā)送消息,則不會(huì)調(diào)用.

面試題:

問題一:+load方法和+ Initialize方法的區(qū)別是什么戒突?

  1. 調(diào)用時(shí)機(jī):load是在Runtime加載類、分類的時(shí)候調(diào)用蘸鲸,只會(huì)調(diào)用一次,Initialize是在類第一次接收到消息時(shí)調(diào)用窿锉,每一個(gè)類只會(huì)初始化一次酌摇。
  2. 調(diào)用方式:load是根據(jù)函數(shù)地址直接調(diào)用膝舅,Initialize是通過objc_msgSend調(diào)用。

問題二:說一下load和Initialize的調(diào)用順序窑多?

對(duì)于load:先調(diào)用父類的+load仍稀,后調(diào)用子類的+load,再調(diào)用分類的+load埂息,并且先編譯的先調(diào)用技潘。

對(duì)于Initialize:先調(diào)用父類的+initialize,再調(diào)用子類的+initialize(先初始化父類千康,再初始化子類)

特別備注

本系列文章總結(jié)自MJ老師在騰訊課堂iOS底層原理班(下)/OC對(duì)象/關(guān)聯(lián)對(duì)象/多線程/內(nèi)存管理/性能優(yōu)化享幽,相關(guān)圖片素材均取自課程中的課件。如有侵權(quán)拾弃,請(qǐng)聯(lián)系我刪除值桩,謝謝!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末豪椿,一起剝皮案震驚了整個(gè)濱河市奔坟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌搭盾,老刑警劉巖咳秉,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鸯隅,居然都是意外死亡澜建,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門滋迈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來霎奢,“玉大人,你說我怎么就攤上這事饼灿∧幌溃” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵碍彭,是天一觀的道長(zhǎng)晤硕。 經(jīng)常有香客問我,道長(zhǎng)庇忌,這世上最難降的妖魔是什么舞箍? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮皆疹,結(jié)果婚禮上疏橄,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好捎迫,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布晃酒。 她就那樣靜靜地躺著,像睡著了一般窄绒。 火紅的嫁衣襯著肌膚如雪贝次。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天彰导,我揣著相機(jī)與錄音蛔翅,去河邊找鬼。 笑死位谋,一個(gè)胖子當(dāng)著我的面吹牛山析,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播倔幼,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼盖腿,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了损同?” 一聲冷哼從身側(cè)響起翩腐,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎膏燃,沒想到半個(gè)月后茂卦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡组哩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年等龙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伶贰。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蛛砰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出黍衙,到底是詐尸還是另有隱情泥畅,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布琅翻,位于F島的核電站位仁,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏方椎。R本人自食惡果不足惜聂抢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望棠众。 院中可真熱鬧琳疏,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至我注,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間迟隅,已是汗流浹背但骨。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留智袭,地道東北人奔缠。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吼野,于是被迫代替她去往敵國(guó)和親校哎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容