iOS-底層-+load和+initialize方法

一. +load方法

1. +load方法調(diào)用順序

調(diào)用時(shí)機(jī):+load方法會(huì)在Runtime加載類、分類時(shí)調(diào)用
調(diào)用順序:先調(diào)用父類的+load,后調(diào)用子類的+load,再調(diào)用分類的+load,并且先編譯的先調(diào)用
調(diào)用方式:根據(jù)函數(shù)地址直接調(diào)用
調(diào)用次數(shù):每個(gè)類、分類的+load方法,在程序運(yùn)行過(guò)程中只調(diào)用一次

首先創(chuàng)建MJStudent繼承于MJPerson,給這兩個(gè)類分別創(chuàng)建兩個(gè)分類,在類和他們的兩個(gè)分類中都重寫(xiě)+load方法,在+load方法中打印,代碼可見(jiàn)文末Demo。

類和分類創(chuàng)建好之后,其他一行代碼不寫(xiě),運(yùn)行項(xiàng)目,打印結(jié)果如下:

MJPerson +load
MJPerson (Test1) +load
MJPerson (Test2) +load
---------------

發(fā)現(xiàn)類和分類的+load方法都有打印。這是因?yàn)橄到y(tǒng)運(yùn)行過(guò)程中只要有這個(gè)類或者分類就會(huì)調(diào)用+load方法,不管你有沒(méi)有使用,而且只會(huì)調(diào)用一次。

2. 驗(yàn)證

+load方法的這一點(diǎn)和其他重寫(xiě)方法不一樣,在Category分類中我們知道,如果重寫(xiě)有相同的方法,會(huì)先調(diào)用分類的方法,后調(diào)用類的方法,并且如果不同分類中有相同的方法,后編譯的分類的方法會(huì)先調(diào)用。

為了驗(yàn)證不同,我們?cè)贛JPerson和它的兩個(gè)分類里面都寫(xiě)上+test方法,然后執(zhí)行如下代碼:

NSLog(@"---------------");
[MJPerson test];

編譯順序如下:

編譯順序.png

打印結(jié)果:

MJPerson +load
MJStudent +load
MJCat +load
MJDog +load
MJPerson (Test2) +load
MJStudent (Test2) +load
MJPerson (Test1) +load
MJStudent (Test1) +load
---------------
MJPerson (Test1) +test

驗(yàn)證結(jié)果:

  1. +load方法都在---------之前,驗(yàn)證了,驗(yàn)證了load方法會(huì)在Runtime加載類、分類時(shí)調(diào)用。
  2. 對(duì)于+load方法,的確是先調(diào)用父類的,后調(diào)用子類的,再調(diào)用分類的,并且先編譯的先調(diào)用,而且每個(gè)類和分類的+load方法都會(huì)調(diào)用。
  3. 每個(gè)類、分類的+load方法,在程序運(yùn)行過(guò)程中只調(diào)用一次。
  4. 對(duì)于+test方法,雖然類和分類中都重寫(xiě)了,但是MJPerson (Test1)是最后編譯的,所以會(huì)先調(diào)用它的+test方法,其他方法被覆蓋了。

3. 源碼分析

首先我們通過(guò)以下方法獲取MJPerson類的所有方法

//打印類對(duì)象里面所有的方法
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);
}

執(zhí)行代碼:

printMethodNamesOfClass(object_getClass([MJPerson class])); //傳入元類對(duì)象

打印如下:

MJPerson load, test, load, test, load, test,

可以發(fā)現(xiàn)所有分類的方法都被加載MJPerson中,但是為什么都調(diào)用的是自己的呢?

下面通過(guò)分析objc4源碼分析一下:

+load方法源碼分析:

objc4源碼解讀過(guò)程:
objc-os.mm文件

_objc_init (運(yùn)行時(shí)入口)

load_images (加載模塊)

prepare_load_methods (準(zhǔn)備load方法)
schedule_class_load (規(guī)劃一些任務(wù))
add_class_to_loadable_list
add_category_to_loadable_list

call_load_methods (調(diào)用load方法)
call_class_loads (調(diào)用類的load方法)
call_category_loads (再調(diào)用分類的load方法)
(*load_method)(cls, SEL_load)

由于源碼閱讀比較復(fù)雜,可按照上面的順序來(lái)閱讀,這里只貼上核心的代碼:

prepare_load_methods方法:

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

    runtimeLock.assertWriting();

    //獲取非懶加載的類(需要加載的類)的列表,然后再調(diào)用schedule_class_load方法
    //所以:先編譯的類先調(diào)用
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        //定制、規(guī)劃一些類的任務(wù)
        schedule_class_load(remapClass(classlist[i]));
    }

    //獲取非懶加載的分類(需要加載的分類)的列表
    //所以:先編譯的分類先調(diào)用
    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());
        //添加分類到可加載列表里面去
        add_category_to_loadable_list(cat);
    }
}

上面的方法,主要是根據(jù)編譯先后獲取可加載的類列表和可加載的分類列表,這兩個(gè)列表會(huì)在call_class_loads和call_category_loads里面用到。

可加載的類獲取完成后,會(huì)進(jìn)入schedule_class_load方法:

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    //這個(gè)方法是遞歸調(diào)用,調(diào)用之前會(huì)先把父類傳進(jìn)來(lái)調(diào)用,然后放到loadable_list數(shù)組里面,直到?jīng)]有父類
    //所以:才會(huì)先調(diào)用父類的load方法,后調(diào)用子類的load方法
    schedule_class_load(cls->superclass);

    //添加類到可加載列表里去
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

這個(gè)方法采用了遞歸調(diào)用,所以會(huì)先把父類添加到可加載類列表里面,再把子類添加到可加載類列表里面。所以最后會(huì)先調(diào)用父類的load方法,后調(diào)用子類的load方法。

可加載類列表和可加載分類列表準(zhǔn)備完畢,下面就進(jìn)入調(diào)用load方法階段。

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) { 
            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;
}

上面代碼可以知道,先調(diào)用類的+load方法在再調(diào)用分類的+load方法。

進(jìn)入call_class_loads方法,這個(gè)方法需要獲取可加載的類的列表,這個(gè)列表就是在prepare_load_methods里面獲取的。

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.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        //直接取出類里面load方法
        //這個(gè)指針直接指向類里面load方法的內(nèi)存地址
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        //直接調(diào)用上面取出的load方法
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

上面代碼可知,直接取出類里面的load方法進(jìn)行調(diào)用的。
并且從可加載類列表里面取的時(shí)候也是從0開(kāi)始取,所以先編譯的類的load方法會(huì)先調(diào)用。

其中l(wèi)oadable_class這個(gè)結(jié)構(gòu)體是可加載的類,里面就一個(gè)load方法的實(shí)現(xiàn),這個(gè)結(jié)構(gòu)體是專門(mén)給load方法使用的,如下:

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

//解釋同上
struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};

再進(jìn)入call_category_loads方法,這個(gè)方法也需要獲取可加載的分類的列表,這個(gè)列表也是在prepare_load_methods里面獲取的。

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    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.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        //直接取出某一個(gè)分類的load方法,拿到內(nèi)存地址
        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));
            }
            //直接根據(jù)拿出的內(nèi)存地址,直接調(diào)用
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }
......
}

可以看出,分類的load方法也是直接取出,直接調(diào)用。
并且從可加載分類列表里面取的時(shí)候也是從0開(kāi)始取,所以先編譯的分類的load方法會(huì)先調(diào)用。

總結(jié):

load方法調(diào)用之前:

  1. 先根據(jù)編譯前后順序獲取可加載類列表
    先把父類添加到可加載類列表里面再把子類添加到可加載類列表里面
  2. 再根據(jù)編譯前后順序獲取可加載分類列表
  3. load方法調(diào)用的時(shí)候,從可加載列表從0開(kāi)始取出類或分類,直接取出它們的load方法進(jìn)行調(diào)用。
  4. +load方法是根據(jù)方法地址直接調(diào)用,并不是經(jīng)過(guò)objc_msgSend函數(shù)調(diào)用(通過(guò)isa和superclass找方法),所以不會(huì)存在方法覆蓋的問(wèn)題。
注意:

上面我們都沒(méi)有主動(dòng)調(diào)用過(guò)load方法,都是讓系統(tǒng)自動(dòng)調(diào)用,系統(tǒng)會(huì)根據(jù)load方法地址,直接調(diào)用。如果我們主動(dòng)調(diào)用了load方法,那走的就是objc_msgSend函數(shù)調(diào)用(通過(guò)isa和superclass找方法)這一套了,具體可以自己想想流程。

二. +initialize方法

1. +initialize方法調(diào)用順序

調(diào)用時(shí)機(jī):+initialize方法會(huì)在類第一次接收到消息時(shí)調(diào)用(走的也是objc_msgSend這一套機(jī)制)
調(diào)用順序:先調(diào)用父類的+initialize,再調(diào)用子類的+initialize(先初始化父類,再初始化子類)
調(diào)用方式:通過(guò)objc_msgSend調(diào)用
調(diào)用次數(shù):每個(gè)類只會(huì)初始化一次

2. 驗(yàn)證

下面用代碼驗(yàn)證一下上面的結(jié)論,首先創(chuàng)建MJStudent繼承于MJPerson,給這兩個(gè)類分別創(chuàng)建兩個(gè)分類,在類和他們的兩個(gè)分類中都重寫(xiě)+initialize方法,在+initialize方法中打印。再創(chuàng)建MJTeacher繼承于MJPerson,不重寫(xiě)任何方法。代碼可見(jiàn)文末Demo。

執(zhí)行如下代碼:

[MJStudent alloc];
[MJStudent alloc];
[MJStudent alloc];
[MJTeacher alloc];

打印結(jié)果如下:

MJPerson (Test2) +initialize
MJStudent (Test1) +initialize
MJPerson (Test2) +initialize

可以發(fā)現(xiàn),MJStudent初始化的時(shí)候會(huì)先調(diào)用MJPerson的initialize,再調(diào)用自己的initialize,而且無(wú)論發(fā)送多少次消息,MJStudent只會(huì)初始化一次。MJTeacher初始化的時(shí)候,由于它自己沒(méi)實(shí)現(xiàn)initialize方法,所以會(huì)去調(diào)用MJPerson的initialize方法。

總結(jié):

  1. 先調(diào)用父類的initialize方法再調(diào)用子類的initialize方法,而且一個(gè)類只會(huì)初始化一次。
  2. 如果子類沒(méi)有實(shí)現(xiàn)+initialize,會(huì)調(diào)用父類的+initialize(所以父類的+initialize可能會(huì)被調(diào)用多次)。
  3. 如果分類實(shí)現(xiàn)了+initialize,就會(huì)覆蓋類本身的+initialize調(diào)用。

3. 源碼分析

下面我們通過(guò)查看objc4源碼看一下為什么是這樣:
+initialize方法源碼分析:

objc4源碼解讀過(guò)程:
objc-msg-arm64.s文件

objc_msgSend

objc-runtime-new.mm文件

class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForward
_class_initialize
callInitialize
objc_msgSend(cls, SEL_initialize)

既然+initialize方法是在類第一次接收到消息時(shí)調(diào)用,我們就先看看objc_msgSend方法里面有沒(méi)有做什么事,首先在objc4里面搜索“objc_msgSend(”,可以發(fā)現(xiàn)objc_msgSend函數(shù)底層是通過(guò)匯編實(shí)現(xiàn)的,匯編看不懂,我們就自己先回顧一下objc_msgSend內(nèi)部尋找方法流程:

isa -> 類對(duì)象\元類對(duì)象,尋找方法,如果找到方法就調(diào)用,如果找不到??
superclass -> 類對(duì)象\元類對(duì)象,尋找方法,如果找到方法就調(diào)用,如果找不到??
superclass -> 類對(duì)象\元類對(duì)象,尋找方法,如果找到方法就調(diào)用,如果找不到??
superclass -> 類對(duì)象\元類對(duì)象,尋找方法,如果找到方法就調(diào)用,如果找不到??
superclass -> 類對(duì)象\元類對(duì)象,尋找方法,如果找到方法就調(diào)用,如果找不到??

更多關(guān)于方法尋找流程,可參考isa指針和superclass指針

直接查看objc_msgSend源碼這條路走不通,我們就換個(gè)方向,找class_getInstanceMethod方法的內(nèi)部實(shí)現(xiàn),這個(gè)函數(shù)傳入一個(gè)類對(duì)象,在類對(duì)象中尋找對(duì)象方法,是C語(yǔ)言寫(xiě)的。

同樣,在objc4搜索“class_getInstanceMethod(”,找到如下方法:

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrNil(cls, sel, nil, 
                   NO/*initialize*/, NO/*cache*/, YES/*resolver*/);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

當(dāng)然,我們也可以搜索“class_getClassMethod(”,查看尋找類方法的內(nèi)部實(shí)現(xiàn):

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

可以發(fā)現(xiàn),這個(gè)方法內(nèi)部也是調(diào)用class_getInstanceMethod,只不過(guò)傳入的不是類對(duì)象而是元類對(duì)象,這和我們以前說(shuō)的“實(shí)例對(duì)象和元類對(duì)象的內(nèi)存結(jié)構(gòu)是一樣的”相吻合。

在class_getInstanceMethod方法中進(jìn)入lookUpImpOrNil

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

......
 //initialize是否需要初始化   !cls->isInitialized這個(gè)類沒(méi)有初始化
 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è)類沒(méi)有初始化,就進(jìn)入_class_initialize方法進(jìn)行初始化,驗(yàn)證了,一個(gè)類只初始化一次。

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

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

上面會(huì)先判斷如果有父類并且父類沒(méi)有初始化就遞歸調(diào)用,初始化父類,如果沒(méi)有父類或者父類已經(jīng)初始化,就開(kāi)始初始化子類。驗(yàn)證了,先初始化父類,再初始化子類。
進(jìn)入callInitialize, 開(kāi)始初始化這個(gè)類

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

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

面試題:

問(wèn)題一:+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是通過(guò)objc_msgSend調(diào)用。

問(wèn)題二:說(shuō)一下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(先初始化父類,再初始化子類)

Demo地址:load和Initialize

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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