面試題
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é)果如下圖。

代碼主要驗(yàn)證下load方法的調(diào)用順序
1>只在Person.m、Person+Test1.m、Person+Test2.m中實(shí)現(xiàn)+load方法,其他.m文件中的+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方法順序。

上圖中可以看出文件的編譯順序是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方法順序。

上圖中可以看出分類文件的編譯順序是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è)源碼解析的流程圖

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

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

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

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

上圖可以看出,每個(gè)類中的+load方法都只會(huì)調(diào)用一次,遞歸的將類和父類都添加到loadable_list中,并且父類會(huì)排在前面。
接下來(lái)再看看add_class_to_loadable_list和add_category_to_loadable_list中具體做了什么

查找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方法邏輯。

這里就可以發(fā)現(xiàn)在調(diào)用load方法時(shí),是優(yōu)先調(diào)用類的+load方法,再調(diào)用分類的+load方法。
call_class_loads和call_category_loads中具體如何執(zhí)行的,我們繼續(xù)向下看。


在類和分類中都是直接找到+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é)果。

結(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è)源碼解析的流程圖

因?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)。

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

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

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

結(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)用父類的load2>再調(diào)用分類的load
a)先編譯的分類,優(yōu)先調(diào)用load2.intialize
1>先初始化父類
2>再初始化子類(可能最終調(diào)用的是父類的initialize方法)