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

一. +load方法

1. +load方法調用順序

調用時機:+load方法會在Runtime加載類瓷翻、分類時調用
調用順序:先調用父類的+load,后調用子類的+load痒给,再調用分類的+load峡懈,并且先編譯的先調用蟀苛。
調用方式:根據(jù)函數(shù)地址直接調用
調用次數(shù):每個類逮诲、分類的+load方法帜平,在程序運行過程中只調用一次

首先創(chuàng)建MJStudent繼承于MJPerson梅鹦,給這兩個類分別創(chuàng)建兩個分類裆甩,在類和他們的兩個分類中都重寫+load方法,在+load方法中打印齐唆,代碼可見文末Demo嗤栓。

類和分類創(chuàng)建好之后,其他一行代碼不寫箍邮,運行項目茉帅,打印結果如下:

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

發(fā)現(xiàn)類和分類的+load方法都有打印。這是因為系統(tǒng)運行過程中只要有這個類或者分類就會調用+load方法锭弊,不管你有沒有使用堪澎,而且只會調用一次。

2. 驗證

+load方法的這一點和其他重寫方法不一樣味滞,在Category分類中我們知道樱蛤,如果重寫有相同的方法钮呀,會先調用分類的方法,后調用類的方法昨凡,并且如果不同分類中有相同的方法爽醋,后編譯的分類的方法會先調用。

為了驗證不同便脊,我們在MJPerson和它的兩個分類里面都寫上+test方法蚂四,然后執(zhí)行如下代碼:

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

編譯順序如下:

編譯順序.png

打印結果:

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

驗證結果:

  1. +load方法都在---------之前,驗證了哪痰,驗證了load方法會在Runtime加載類证杭、分類時調用。
  2. 對于+load方法妒御,的確是先調用父類的解愤,后調用子類的,再調用分類的乎莉,并且先編譯的先調用送讲,而且每個類和分類的+load方法都會調用。
  3. 每個類惋啃、分類的+load方法哼鬓,在程序運行過程中只調用一次。
  4. 對于+test方法边灭,雖然類和分類中都重寫了异希,但是MJPerson (Test1)是最后編譯的,所以會先調用它的+test方法绒瘦,其他方法被覆蓋了称簿。

3. 源碼分析

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

//打印類對象里面所有的方法
void printMethodNamesOfClass(Class cls)
{
    unsigned int count;
    // 獲得方法數(shù)組
    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);
}

執(zhí)行代碼:

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

打印如下:

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

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

下面通過分析objc4源碼分析一下:

+load方法源碼分析:

objc4源碼解讀過程:
objc-os.mm文件

_objc_init (運行時入口)

load_images (加載模塊)

prepare_load_methods (準備load方法)
schedule_class_load (規(guī)劃一些任務)
add_class_to_loadable_list
add_category_to_loadable_list

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

由于源碼閱讀比較復雜憨降,可按照上面的順序來閱讀,這里只貼上核心的代碼:

prepare_load_methods方法:

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

    runtimeLock.assertWriting();

    //獲取非懶加載的類(需要加載的類)的列表该酗,然后再調用schedule_class_load方法
    //所以:先編譯的類先調用
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        //定制授药、規(guī)劃一些類的任務
        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());
        //添加分類到可加載列表里面去
        add_category_to_loadable_list(cat);
    }
}

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

可加載的類獲取完成后,會進入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;

    //這個方法是遞歸調用爵嗅,調用之前會先把父類傳進來調用娇澎,然后放到loadable_list數(shù)組里面,直到?jīng)]有父類
    //所以:才會先調用父類的load方法操骡,后調用子類的load方法
    schedule_class_load(cls->superclass);

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

這個方法采用了遞歸調用九火,所以會先把父類添加到可加載類列表里面,再把子類添加到可加載類列表里面册招。所以最后會先調用父類的load方法岔激,后調用子類的load方法。

可加載類列表和可加載分類列表準備完畢是掰,下面就進入調用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(); //先調用類的+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;
}

上面代碼可以知道,先調用類的+load方法在再調用分類的+load方法键痛。

進入call_class_loads方法炫彩,這個方法需要獲取可加載的類的列表,這個列表就是在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方法
        //這個指針直接指向類里面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());
        }
        //直接調用上面取出的load方法
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

上面代碼可知江兢,直接取出類里面的load方法進行調用的。
并且從可加載類列表里面取的時候也是從0開始取丁频,所以先編譯的類的load方法會先調用杉允。

其中l(wèi)oadable_class這個結構體是可加載的類,里面就一個load方法的實現(xiàn)席里,這個結構體是專門給load方法使用的叔磷,如下:

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

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

再進入call_category_loads方法,這個方法也需要獲取可加載的分類的列表奖磁,這個列表也是在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;
        //直接取出某一個分類的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)存地址咖为,直接調用
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }
......
}

可以看出秕狰,分類的load方法也是直接取出,直接調用躁染。
并且從可加載分類列表里面取的時候也是從0開始取封恰,所以先編譯的分類的load方法會先調用。

總結:

load方法調用之前:

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

上面我們都沒有主動調用過load方法懂盐,都是讓系統(tǒng)自動調用褥赊,系統(tǒng)會根據(jù)load方法地址,直接調用莉恼。如果我們主動調用了load方法拌喉,那走的就是objc_msgSend函數(shù)調用(通過isa和superclass找方法)這一套了速那,具體可以自己想想流程。

二. +initialize方法

1. +initialize方法調用順序

調用時機:+initialize方法會在類第一次接收到消息時調用(走的也是objc_msgSend這一套機制)尿背。
調用順序:先調用父類的+initialize端仰,再調用子類的+initialize(先初始化父類,再初始化子類)田藐。
調用方式:通過objc_msgSend調用荔烧。
調用次數(shù):每個類只會初始化一次

2. 驗證

下面用代碼驗證一下上面的結論汽久,首先創(chuàng)建MJStudent繼承于MJPerson鹤竭,給這兩個類分別創(chuàng)建兩個分類,在類和他們的兩個分類中都重寫+initialize方法景醇,在+initialize方法中打印臀稚。再創(chuàng)建MJTeacher繼承于MJPerson,不重寫任何方法三痰。代碼可見文末Demo烁涌。

執(zhí)行如下代碼:

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

打印結果如下:

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

可以發(fā)現(xiàn),MJStudent初始化的時候會先調用MJPerson的initialize酒觅,再調用自己的initialize撮执,而且無論發(fā)送多少次消息,MJStudent只會初始化一次舷丹。MJTeacher初始化的時候抒钱,由于它自己沒實現(xiàn)initialize方法,所以會去調用MJPerson的initialize方法颜凯。

總結:

  1. 先調用父類的initialize方法再調用子類的initialize方法谋币,而且一個類只會初始化一次。
  2. 如果子類沒有實現(xiàn)+initialize症概,會調用父類的+initialize(所以父類的+initialize可能會被調用多次)蕾额。
  3. 如果分類實現(xiàn)了+initialize,就會覆蓋類本身的+initialize調用彼城。

3. 源碼分析

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

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

objc_msgSend

objc-runtime-new.mm文件

class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForward
_class_initialize
callInitialize
objc_msgSend(cls, SEL_initialize)

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

isa -> 類對象\元類對象舱馅,尋找方法缰泡,如果找到方法就調用,如果找不到??
superclass -> 類對象\元類對象代嗤,尋找方法棘钞,如果找到方法就調用缠借,如果找不到??
superclass -> 類對象\元類對象,尋找方法宜猜,如果找到方法就調用泼返,如果找不到??
superclass -> 類對象\元類對象,尋找方法宝恶,如果找到方法就調用符隙,如果找不到??
superclass -> 類對象\元類對象趴捅,尋找方法垫毙,如果找到方法就調用,如果找不到??

更多關于方法尋找流程拱绑,可參考isa指針和superclass指針

直接查看objc_msgSend源碼這條路走不通综芥,我們就換個方向,找class_getInstanceMethod方法的內(nèi)部實現(xiàn)猎拨,這個函數(shù)傳入一個類對象膀藐,在類對象中尋找對象方法,是C語言寫的红省。

同樣额各,在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);
}

當然吧恃,我們也可以搜索“class_getClassMethod(”虾啦,查看尋找類方法的內(nèi)部實現(xiàn):

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

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

可以發(fā)現(xiàn),這個方法內(nèi)部也是調用class_getInstanceMethod痕寓,只不過傳入的不是類對象而是元類對象傲醉,這和我們以前說的“實例對象和元類對象的內(nèi)存結構是一樣的”相吻合。

在class_getInstanceMethod方法中進入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;
}

再進入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方法進行初始化硬毕,驗證了,一個類只初始化一次礼仗。

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

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

上面會先判斷如果有父類并且父類沒有初始化就遞歸調用挪丢,初始化父類,如果沒有父類或者父類已經(jīng)初始化卢厂,就開始初始化子類乾蓬。驗證了,先初始化父類慎恒,再初始化子類任内。
進入callInitialize撵渡, 開始初始化這個類

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

通過上面的源碼分析死嗦,可以知道趋距,的確是先調用父類的Initialize再調用子類的Initialize,并且一個類只會初始化一次越除。

面試題:

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

  1. 調用時機:load是在Runtime加載類、分類的時候調用摘盆,只會調用一次翼雀,Initialize是在類第一次接收到消息時調用,每一個類只會初始化一次孩擂。
  2. 調用方式:load是根據(jù)函數(shù)地址直接調用狼渊,Initialize是通過objc_msgSend調用。

問題二:說一下load和Initialize的調用順序类垦?

對于load:先調用父類的+load狈邑,后調用子類的+load,再調用分類的+load蚤认,并且先編譯的先調用米苹。
對于Initialize:先調用父類的+initialize,再調用子類的+initialize(先初始化父類砰琢,再初始化子類)蘸嘶。

Demo地址:load和Initialize

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市氯析,隨后出現(xiàn)的幾起案子亏较,更是在濱河造成了極大的恐慌,老刑警劉巖掩缓,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雪情,死亡現(xiàn)場離奇詭異,居然都是意外死亡你辣,警方通過查閱死者的電腦和手機巡通,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舍哄,“玉大人宴凉,你說我怎么就攤上這事”硇” “怎么了弥锄?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我籽暇,道長温治,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任戒悠,我火速辦了婚禮熬荆,結果婚禮上,老公的妹妹穿的比我還像新娘绸狐。我一直安慰自己卤恳,他們只是感情好,可當我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布寒矿。 她就那樣靜靜地躺著突琳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪劫窒。 梳的紋絲不亂的頭發(fā)上本今,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天拆座,我揣著相機與錄音主巍,去河邊找鬼。 笑死挪凑,一個胖子當著我的面吹牛孕索,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播躏碳,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼搞旭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了菇绵?” 一聲冷哼從身側響起肄渗,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎咬最,沒想到半個月后翎嫡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡永乌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年惑申,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翅雏。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡圈驼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出望几,到底是詐尸還是另有隱情绩脆,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站靴迫,受9級特大地震影響祈坠,放射性物質發(fā)生泄漏。R本人自食惡果不足惜矢劲,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一赦拘、第九天 我趴在偏房一處隱蔽的房頂上張望湿诊。 院中可真熱鬧惦银,春花似錦、人聲如沸聘惦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至黄刚,卻和暖如春捎谨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背憔维。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工涛救, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人业扒。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓检吆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親程储。 傳聞我的和親對象是個殘疾皇子蹭沛,可洞房花燭夜當晚...
    茶點故事閱讀 44,654評論 2 354

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