Category-load、initialize調(diào)用原理

面試題

load、initialize方法的區(qū)別是什么?他們?cè)贑ategory中的調(diào)用順序?

load調(diào)用原理

1.+load方法會(huì)在runtime加載類、分類的時(shí)候調(diào)用,系統(tǒng)會(huì)主動(dòng)調(diào)用

2.每個(gè)類、分類的+load,在程序運(yùn)行中只會(huì)調(diào)用一次

3.調(diào)用順序
1>先調(diào)用父類的+load
a)按照編譯順序先后調(diào)用(先編譯,先調(diào)用)
b)調(diào)用子類的+load之前會(huì)先調(diào)用父類的+load

2>再調(diào)用分類的+load
a)按照編譯先后順序調(diào)用(先編譯,先調(diào)用)

首先給出結(jié)論,接下來(lái)通過(guò)代碼驗(yàn)證和源碼分析。

load代碼驗(yàn)證

先來(lái)一段代碼,分析load方法的調(diào)用情況。
創(chuàng)建Person類繼承至NSObject,Person+Test1分類,Person+Test2分類;再創(chuàng)建Student類繼承至Person,Student+Test1分類,Person+Test2分類;最后創(chuàng)建Dog也是繼承至NSObject,與Person類對(duì)照。

@interface Person : NSObject

@end

@implementation Person

+ (void)load{
    NSLog(@"Person +load");
}

@end



@interface Person (Test1)

@end

@implementation Person (Test1)

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

@end



@interface Person (Test2)

@end

@implementation Person (Test2)

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

@end



@interface Student : Person

@end

@implementation Student

+ (void)load{
    NSLog(@"Student +load");
}

@end



@interface Student (Test1)

@end

@implementation Student (Test1)

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

@end



@interface Student (Test2)

@end

@implementation Student (Test2)

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

@end


@interface Dog : NSObject

@end

+ (void)load{
    NSLog(@"Dog +load");
}

運(yùn)行上面代碼,在外部不調(diào)用Person,Student,Dog中的方法,每個(gè).m文件中+load都調(diào)用了一遍。運(yùn)行結(jié)果如下圖。


+load調(diào)用順序

代碼主要驗(yàn)證下load方法的調(diào)用順序
1>只在Person.m、Person+Test1.m、Person+Test2.m中實(shí)現(xiàn)+load方法,其他.m文件中的+load方法都屏蔽掉,觀察調(diào)用類和分類的+load方法順序。


調(diào)用類和分類中+load順序

運(yùn)行結(jié)果顯示先調(diào)用類的+load,再調(diào)用分類的+load。

2>只在Person.m、Student.m、Dog.m中實(shí)現(xiàn)+load方法,其他.m文件中的+load方法都屏蔽掉,觀察沒有分類時(shí),調(diào)用類的+load方法順序。


調(diào)用類中+load順序

上圖中可以看出文件的編譯順序是Dog.m->Student.m->Person.m,其中Student繼承至Person。而運(yùn)行結(jié)果Dog中+load調(diào)用先于Person,說(shuō)明調(diào)用類中+load方法是按照編譯順序調(diào)用,先編譯先調(diào)用。Student的編譯順序先于Person,為什么調(diào)用順序反而在后面呢?這是因?yàn)镾tudent繼承至Person,調(diào)用子類的+load前會(huì)先調(diào)用父類的+load。

3>只在Persson+Test1.m、Persson+Test2.m、Student+Test1.m、Student +Test2.m中實(shí)現(xiàn)+load方法,其他.m文件中的load方法都屏蔽掉,觀察調(diào)用分類的+load方法順序。


調(diào)用分類中+load順序

上圖中可以看出分類文件的編譯順序是Person+Test1.m->Person+Test2.m->Student+Test1.m-> Student +Test2.m,而運(yùn)行的結(jié)果和編譯順序是一樣的。說(shuō)明調(diào)用分類的+load是按照編譯順序,先編譯先調(diào)用。

從上面的三步分別驗(yàn)證,再看第一次運(yùn)行的結(jié)果截圖,這個(gè)順序是完全符合的。這樣也就驗(yàn)證了最開頭的+load調(diào)用順序的總結(jié)。并且在外部完全不調(diào)用+load方法的時(shí)候,+load方法依然會(huì)被調(diào)用,其實(shí)就是runtime在加載類和分類的時(shí)候就主動(dòng)調(diào)用了+load方法,同時(shí)結(jié)合以上運(yùn)行結(jié)果,程序運(yùn)行過(guò)程中只會(huì)調(diào)用一次+load方法。

load源碼分析

為什么調(diào)用+load會(huì)出現(xiàn)以上的規(guī)律呢?我們通過(guò)runtime源碼來(lái)一探究竟。

先貼一個(gè)源碼解析的流程圖


+load源碼解析流程

首先來(lái)到runtime的初始化方法,在objc-os.mm中搜索_objc_init。

runtime初始化函數(shù)

再來(lái)到load_images,這個(gè)函數(shù)中主動(dòng)調(diào)用了load方法。

load_images函數(shù)

我們先看看系統(tǒng)是怎么去查找load方法的,進(jìn)入到prepare_load_methods函數(shù)。

prepare_load_methods函數(shù).png

這里發(fā)現(xiàn)類和分類都是分別按照編譯的順序取出來(lái),分類取出來(lái)之后就直接按編譯順序放到了一個(gè)loadable_list中,而類取出來(lái)中又調(diào)用了schedule_class_load函數(shù),在這個(gè)函數(shù)中其實(shí)是給類和父類調(diào)用順序排序。

schedule_class_load函數(shù)

上圖可以看出,每個(gè)類中的+load方法都只會(huì)調(diào)用一次,遞歸的將類和父類都添加到loadable_list中,并且父類會(huì)排在前面。

接下來(lái)再看看add_class_to_loadable_listadd_category_to_loadable_list中具體做了什么

add_class_to_loadable_list函數(shù)和add_category_to_loadable_list函數(shù)

查找load方法的邏輯總結(jié)(prepare_load_methods)

類和其對(duì)應(yīng)的load方法,賦值給loadable_class,最后統(tǒng)一添加到loadable_classes中
順序是按文件編譯的順序,但是父類會(huì)強(qiáng)制排在子類前面,并且每個(gè)類只會(huì)被添加一次

分類和其對(duì)應(yīng)的load方法,賦值給loadable_category,最后統(tǒng)一添加到loadable_categories中
順序就是按編譯的順序

接著在來(lái)看call_load_methods,調(diào)用load方法邏輯。

call_load_methods函數(shù)

這里就可以發(fā)現(xiàn)在調(diào)用load方法時(shí),是優(yōu)先調(diào)用類的+load方法,再調(diào)用分類的+load方法。

call_class_loadscall_category_loads中具體如何執(zhí)行的,我們繼續(xù)向下看。

call_class_loads函數(shù)

call_category_loads函數(shù)

在類和分類中都是直接找到+load方法然后調(diào)用。所以不存在先調(diào)用調(diào)用子類的+load,就不調(diào)用父類的+load,也不存在先調(diào)用分類的+load,就不調(diào)用原本類中的+load。類和分類中的+load都會(huì)在runtime初始化時(shí)主動(dòng)被系統(tǒng)調(diào)用,并且在運(yùn)行過(guò)程中只調(diào)用一次。

initialize調(diào)用原理

1.+initialize方法會(huì)在類第一次接收到消息時(shí)調(diào)用

2.調(diào)用順序
a)先調(diào)用父類的+initialize,再調(diào)用子類的+initialize(先初始化父類,再初始化子類,每個(gè)類只會(huì)初始化一次)

+initialize是通過(guò)objc_msgSend進(jìn)行調(diào)用的,所以有以下特點(diǎn)
a)如果子類沒有實(shí)現(xiàn)+initialize,會(huì)調(diào)用父類的+initialize(所以父類的+initialize可能會(huì)被調(diào)用多次)
b)如果分類實(shí)現(xiàn)了+initialize,就會(huì)覆蓋類本身的+initialize調(diào)用

接下來(lái)通過(guò)代碼驗(yàn)證和源碼分析。

代碼驗(yàn)證
@interface Person : NSObject

@end

@implementation Person

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

@end


@interface Person (Test1)

@end

@implementation Person (Test1)

+ (void)initialize{
    NSLog(@"Person (Test1) +initialize");
}

@end


@interface Person (Test2)

@end

@implementation Person (Test2)

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

@end


@interface Student : Person

@end

@implementation Student

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

@end


@interface Student (Test1)

@end

@implementation Student (Test1)

+ (void)initialize{
    NSLog(@"Student (Test1) +initialize");
}

@end


@interface Student (Test2)

@end

@implementation Student (Test2)

+ (void)initialize{
    NSLog(@"Student (Test1) +initialize");
}

@end


@interface Dog : NSObject

@end

@implementation Dog

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

@end

1>以上代碼,在外部不調(diào)用所有類和分類,運(yùn)行結(jié)果是沒有調(diào)用任何一個(gè)+initialize方法。

2.0>只在Person.m、Student.m、Dog.m中實(shí)現(xiàn)+initialize方法,其他.m文件中的+initialize方法都屏蔽掉,在外部調(diào)用[Person alloc];[Student alloc]; [Dog alloc];,分別給Person類發(fā)送了alloc,給Student類發(fā)送了alloc,給Dog類發(fā)送了alloc消息,觀察運(yùn)行結(jié)果。

2.1>在外部調(diào)用[Person alloc];[Student alloc]; [Dog alloc];[Person alloc];[Student alloc]; [Dog alloc],多次分別給Person類發(fā)送了alloc,給Student類發(fā)送了alloc,給Dog類發(fā)送了alloc消息,與2.0作為對(duì)照,觀察運(yùn)行結(jié)果。

2.2>在外部調(diào)用[Student alloc];[Person alloc];[Dog alloc];,調(diào)換Student和Person發(fā)送alloc消息的順序,同樣與2.0作為對(duì)照,觀察運(yùn)行結(jié)果。

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Student.h"
#import "Dog.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //2.0
        [Person alloc];
        [Student alloc];
        [Dog alloc];
        
        //2.1
        [Person alloc];
        [Student alloc];
        [Dog alloc];
        
        [Person alloc];
        [Student alloc];
        [Dog alloc];
        
        [Person alloc];
        [Student alloc];
        [Dog alloc];
        
        //2.2
        [Student alloc];
        [Person alloc];
        [Dog alloc];
        
    }
    return 0;
}

運(yùn)行結(jié)果
2019-07-11 20:58:54.031975+0800 Category-initialize[12113:4824617] Person +initialize
2019-07-11 20:58:54.032140+0800 Category-initialize[12113:4824617] Student +initialize
2019-07-11 20:58:54.032152+0800 Category-initialize[12113:4824617] Dog +initialize

以上2.0,2.1,2.2三種運(yùn)行結(jié)果都是一致。
通過(guò)以上四種情況的結(jié)果,說(shuō)明+initialize方法會(huì)在類第一次接收到消息的時(shí)候調(diào)用。并且會(huì)先調(diào)用父類的+initialize,再調(diào)用子類的+initialize。

3>只在Person.m中實(shí)現(xiàn)+initialize方法,其他所有.m中的+initialize方法都屏蔽,包括Person的子類Student。在外部調(diào)用[Student alloc];,觀察運(yùn)行結(jié)果。

2019-07-11 21:28:06.730804+0800 Category-initialize[12336:4875791] Person +initialize
2019-07-11 21:28:06.730959+0800 Category-initialize[12336:4875791] Person +initialize

結(jié)果是調(diào)用了兩次父類Person中的+int initialize,進(jìn)一步說(shuō)明先調(diào)用父類的+initialize,再調(diào)用子類的+initialize,同時(shí)子類沒有實(shí)現(xiàn)+initialize,會(huì)調(diào)用父類的+initialize。

4>每個(gè).m中都實(shí)現(xiàn)+initialize,在外部調(diào)用[Person alloc];[Student alloc]; [Dog alloc];,觀察運(yùn)行結(jié)果。


+initialize調(diào)用順序

結(jié)果調(diào)用了Person+Test1和Student+Test1中的+initialize方法。也說(shuō)明了如果分類實(shí)現(xiàn)了+initialize,就覆蓋類本身的+initialize調(diào)用。而多個(gè)分類中的調(diào)用順序是,后編譯先調(diào)用,都是符合的。

initialize源碼分析

為什么調(diào)用+initialize會(huì)出現(xiàn)以上的規(guī)律呢?我們也通過(guò)runtime源碼來(lái)一探究竟。

先貼一個(gè)源碼解析的流程圖


+initialize源碼解析流程

因?yàn)?initialize是在類第一次接收到消息時(shí)調(diào)用,那底層一定是調(diào)用了objc_msgSend,相當(dāng)于objc_msgSend(cls,@selector(@"alloc"))給類cls發(fā)送了一條alloc消息。 在runtime源碼中搜索objc_msgSend,結(jié)果在objc-msg-arm64.s中發(fā)現(xiàn)其是通過(guò)匯編實(shí)現(xiàn)的。無(wú)法看懂匯編的情況下我們只能先行分析,發(fā)送消息,通過(guò)isa找到類,然后要經(jīng)歷查找方法和調(diào)用方法兩個(gè)步驟,而+initialize就可能是在這兩個(gè)過(guò)程中調(diào)用的。

我們通過(guò)XCode斷點(diǎn)alloc方法,然后顯示匯編來(lái)查看匯編中查找方法和調(diào)用方法的流程。步驟Debug->Debug workflow->Always Show Disassembly,找到callq-msgSend并且斷點(diǎn),跳到斷點(diǎn)處,control+stepinto進(jìn)入到實(shí)現(xiàn)內(nèi)部,發(fā)現(xiàn)最后回來(lái)到_objc_msgSend_uncached,斷點(diǎn)并跳到此處,control+stepinto進(jìn)入到實(shí)現(xiàn)內(nèi)部,我們終于找到了一個(gè)不是匯編的函數(shù)_class_lookupMethodAndLoadCache3,在runtime源碼中搜索找到該方法在objc-runtime-new.mm中,我們就來(lái)順著這個(gè)方法看看內(nèi)部的實(shí)現(xiàn)。

_class_lookupMethodAndLoadCache3函數(shù)

接下來(lái)進(jìn)入lookUpImpOrForward函數(shù)內(nèi)部。

lookUpImpOrForward函數(shù)

再接著進(jìn)入到_class_initialize函數(shù)內(nèi)部。

_class_initialize函數(shù)

最后來(lái)到callInitialize函數(shù)內(nèi)部,發(fā)現(xiàn)+initialize就是通過(guò)objc_msgSend進(jìn)行調(diào)用的。

callInitialize函數(shù)

結(jié)合底層源碼,也都一一驗(yàn)證了關(guān)于+initialize調(diào)用原理的總結(jié)。為什么是在類第一次收到消息時(shí)調(diào)用?為什么調(diào)用子類的+initialize會(huì)先調(diào)用父類的+initialize?以及+initialize調(diào)用的兩個(gè)特點(diǎn),都能得到解答。

接下來(lái)進(jìn)行面試題的總結(jié)。

load、initialize方法的區(qū)別是什么?他們?cè)贑ategory中的調(diào)用順序?
1.調(diào)用方式
1>load是根據(jù)函數(shù)地址調(diào)用
2>initialize是通過(guò)objc_msgSend調(diào)用

2.調(diào)用時(shí)刻
1>load是runtime加載類、分類的時(shí)候調(diào)用(只會(huì)調(diào)用一次)
2>initialize是類第一次接收到消息的時(shí)候調(diào)用,每一個(gè)類只會(huì)initialize一次(父類的initialize方法可能會(huì)調(diào)用多次)

load、initialize調(diào)用順序
1.load
1>先調(diào)用類的load
a)先編譯的類,優(yōu)先調(diào)用load
b)調(diào)用子類的load之前,會(huì)優(yōu)先調(diào)用父類的load

2>再調(diào)用分類的load
a)先編譯的分類,優(yōu)先調(diào)用load

2.intialize
1>先初始化父類
2>再初始化子類(可能最終調(diào)用的是父類的initialize方法)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容