OC文件在編譯后反浓,類相關(guān)的數(shù)據(jù)結(jié)構(gòu)會(huì)保留在目標(biāo)文件中,在運(yùn)行時(shí)得到解析和使用赋焕。在應(yīng)用程序運(yùn)行起來的時(shí)候耙厚,類的信息會(huì)有加載和初始化過程强挫,這個(gè)過程就涉及到了類的兩個(gè)類方法:load
和initialize
。下面我們就來介紹一下這2個(gè)方法的區(qū)別薛躬。(首先要說明一下俯渤,這2個(gè)方法是系統(tǒng)調(diào)用的,開發(fā)者一般不會(huì)主動(dòng)去調(diào)用者兩個(gè)方法型宝,這么做也沒有什么意義八匠,所以后面的講解都是針對(duì)系統(tǒng)調(diào)用,不考慮主動(dòng)調(diào)用的情況)趴酣。
1. load方法
1.1 調(diào)用時(shí)機(jī)
當(dāng)我們啟動(dòng)程序時(shí)梨树,參與了編譯的類、分類都會(huì)被加載進(jìn)內(nèi)存岖寞,load
方法就是在這個(gè)類被加載的時(shí)候調(diào)用的(前提是這個(gè)類有實(shí)現(xiàn)load
方法)抡四,這個(gè)過程與這個(gè)類是否被使用是無關(guān)的,也就是說如果有一個(gè)類(MyClass)即使在整個(gè)程序中都沒有用到仗谆,甚至沒有任何一個(gè)文件去引入MyClass的頭文件床嫌,MyClass的的load
的方法一樣會(huì)被調(diào)用跨释。等所有的類胸私、分類都加載進(jìn)內(nèi)存后才會(huì)調(diào)用程序的main
函數(shù)厌处,所以所有類的load
方法都是在main
函數(shù)之前被調(diào)用的。而且每個(gè)類岁疼、分類的load
方法只會(huì)被調(diào)用一次阔涉。
1.2 調(diào)用順序
一個(gè)程序中如果所有的類、分類都實(shí)現(xiàn)了load
方法捷绒,那么所有的load
方法都會(huì)被調(diào)用瑰排。它們的執(zhí)行順序遵循以下規(guī)則:
- 先執(zhí)行所有類的
load
方法,再執(zhí)行所有分類的load
方法暖侨。 - 執(zhí)行類的
load
方法時(shí)椭住,是按照參與編譯的順序,先編譯的類先執(zhí)行字逗,但是如果某個(gè)類是繼承自另一個(gè)類京郑,那么會(huì)先執(zhí)行父類的load
方法個(gè)再執(zhí)行自己的load
方法。 - 執(zhí)行分類的
load
方法時(shí)葫掉,是按照分類參與編譯的順序些举,先編譯的分類先執(zhí)行。
關(guān)于編譯順序俭厚,我們可以在項(xiàng)目的Build Phases
--> Compile Sources
查看户魏,最上面的就最先編譯,我們可以拖動(dòng)文件來調(diào)整編譯順序挪挤。下面舉個(gè)例子來看下load
方法的執(zhí)行順序叼丑。首先說明一下幾個(gè)類的關(guān)系:Person
類有aaa
和bbb
兩個(gè)分類,men
類繼承自Person
類扛门,men
也有2個(gè)分類ccc
和ddd
鸠信,Book
類和前面這些類沒有任何關(guān)系。
解釋:
- 編譯順序從上到下尖飞,上面先編譯症副,下面后編譯。由于先執(zhí)行類的
load
再執(zhí)行分類的load
政基,最先參與編譯的類是men
贞铣,而men
繼承自Person
,所以最先執(zhí)行Person
的load
(雖然Person
是后參與編譯的沮明,但是它是父類辕坝,所以會(huì)先執(zhí)行),然后再執(zhí)行men
的load
荐健。接著參與編譯的是Book
類酱畅,所以緊接著就是執(zhí)行Book
的load
琳袄。再接著參與編譯的類就是Person
,由于它的load
方法已經(jīng)執(zhí)行過了纺酸,此時(shí)就不會(huì)執(zhí)行了窖逗。 - 所有的類的
load
方法都執(zhí)行完后開始執(zhí)行分類的load
,分類參與編譯的順序是men+ccc
-->Person+aaa
-->men+ddd
-->Person+bbb
餐蔬,所以分類的load
方法個(gè)也是按照這個(gè)順序執(zhí)行碎紊。
1.3 執(zhí)行方式
我們知道,當(dāng)分類中存在和本類中同名的方法時(shí)樊诺,調(diào)用這個(gè)方法最終執(zhí)行的是分類中的方法仗考。那上面就很奇怪了,Person
和Person的分類
中都有load
方法词爬,按理說調(diào)用load
方法時(shí)最終只會(huì)調(diào)用其中一個(gè)分類的load
方法秃嗜,可結(jié)果Person
本類和它的2個(gè)分類都調(diào)用了load
方法。這是因?yàn)?code>load方法和普通方法調(diào)用的方式不一樣顿膨。普通方法調(diào)用是通過消息發(fā)送機(jī)制實(shí)現(xiàn)的锅锨,會(huì)先去類或元類的方法列表中查找,如果找到了方法就執(zhí)行虽惭,如果沒有找到就去父類的方法列表里面找橡类,只要找到就會(huì)終止查找,所以只會(huì)執(zhí)行一次芽唇。而load
方法調(diào)用時(shí)顾画,每個(gè)類都是根據(jù)load
方法的地址直接調(diào)用,而不會(huì)走objc_msgSend
函數(shù)的方法查找流程匆笤,也就是說一個(gè)類有實(shí)現(xiàn)load
方法就執(zhí)行研侣,沒有就不執(zhí)行(沒有的話也不會(huì)去父類里面查找)。想要了解更加詳細(xì)的底層實(shí)現(xiàn)流程炮捧,可以去看objc4源碼庶诡,https://opensource.apple.com/tarballs/objc4/這里提供一下相關(guān)函數(shù)調(diào)用流程以便進(jìn)行源碼閱讀: 首先從objc-os.mm
文件的_objc_init
函數(shù)開始-->load_images
-->prepare_load_methods
-->schedule_class_load
-->add_class_to_loadable_list
-->add_category_to_loadable_list
-->call_load_methods
-->call_class_loads
-->call_category_loads
-->(*load_method)(cls, SEL_load)
。
1.4 實(shí)現(xiàn)load方法時(shí)要注意什么
我們通常在load
方法中進(jìn)行方法交換(Method Swizzle)咆课,除此之外末誓,除非真的有必要,我們盡量不要在load
方法中寫代碼书蚪,尤其不要在load
方法中使用其它的類喇澡,因?yàn)檫@個(gè)時(shí)候其它的類可能還沒有被加載進(jìn)內(nèi)存,隨意使用可能會(huì)出問題殊校。如果確實(shí)要在load
方法寫一些代碼晴玖,那也要盡量精簡(jiǎn)代碼,不要做一些耗時(shí)或者等待鎖的操作,因?yàn)檎麄€(gè)程序在執(zhí)行load
方法時(shí)都會(huì)阻塞呕屎,從而導(dǎo)致程序啟動(dòng)時(shí)間過長(zhǎng)甚至無法啟動(dòng)让簿。
2. initialize方法
2.1 調(diào)用時(shí)機(jī)
initialize
方法是在類或它的子類收到第一條消息時(shí)被調(diào)用的,這里的消息就是指實(shí)例方法或類方法的調(diào)用秀睛,所以所有類的initialize
調(diào)用是在執(zhí)行main
函數(shù)之后調(diào)用的尔当。而且一個(gè)類只會(huì)調(diào)用一次initialize
方法。如果一個(gè)類在程序運(yùn)行過程中一直沒有被使用過琅催,那這個(gè)類的initialize
方法也就不會(huì)被調(diào)用居凶,這一點(diǎn)和load
方法是不一樣的。
2.2 調(diào)用方式
initialize
方法的調(diào)用和普通方法調(diào)用一樣藤抡,也是走的objc_msgSend
流程。所以如果一個(gè)類和它的分類都實(shí)現(xiàn)了initialize
方法抹估,那最終調(diào)用的會(huì)是分類中的方法缠黍。如果子類和父類都實(shí)現(xiàn)了initialize
方法,那么會(huì)先調(diào)用父類的方法药蜻,然后調(diào)用子類的方法個(gè)(這里注意子類中不需要寫[super initialize]
來調(diào)用父類的方法瓷式,通過查看源碼得知它是在底層實(shí)現(xiàn)過程中主動(dòng)調(diào)用的父類的initialize
方法)。下面看一個(gè)例子:父類Person
實(shí)現(xiàn)了initialize
语泽,PersonSub1
和PersonSub2
這兩個(gè)子類也實(shí)現(xiàn)了initialize
贸典,PersonSub3
和PersonSub4
這兩個(gè)子類沒有實(shí)現(xiàn)了initialize
,按照下面的順序?qū)嵗瘜?duì)象:
PersonSub1 *ps1 = [[PersonSub1 alloc] init];
Person *person = [[Person alloc] init];
PersonSub2 *ps2 = [[PersonSub2 alloc] init];
PersonSub3 *ps3 = [[PersonSub3 alloc] init];
PersonSub4 *ps4 = [[PersonSub4 alloc] init];
// ***************打印結(jié)果***************
2020-01-06 15:52:38.429218+0800 CommandLine[68706:7207027] +[Person initialize]
2020-01-06 15:52:38.429250+0800 CommandLine[68706:7207027] +[PersonSub1 initialize]
2020-01-06 15:52:38.429287+0800 CommandLine[68706:7207027] +[PersonSub2 initialize]
2020-01-06 15:52:38.429347+0800 CommandLine[68706:7207027] +[Person initialize]
2020-01-06 15:52:38.429380+0800 CommandLine[68706:7207027] +[Person initialize]
看到這個(gè)運(yùn)行結(jié)果踱卵,有人就有疑問了:不是說一個(gè)類只會(huì)調(diào)用一次initialize
方法嗎廊驼,為什么這里Person
的initialize
方法被調(diào)用了3次?這里就需要講解一下底層源碼的執(zhí)行流程了惋砂,每個(gè)類都有一個(gè)標(biāo)記記錄這個(gè)類是否調(diào)用過initialize
妒挎,我這里就用一個(gè)BOOL類型的isInitialized
來表示,然后用selfClass
來表示自己的類西饵,用superClass
來表示父類酝掩,下面我用偽代碼來描述一下底層源碼執(zhí)行流程:
// 如果自己沒有調(diào)用過initialize就執(zhí)行里面的代碼
if(!selfClass.isInitialized){
if(!superClass.isInitialized){
// 如果父類沒有執(zhí)行過initialize就給父類發(fā)消息(一旦成功執(zhí)行initialize就將父類的isInitialized置為YES)
objc_msgSend(superClass,@selector(initialize));
}
// 再給自己的類發(fā)消息(一旦成功執(zhí)行initialize就將自己類的isInitialized置為YES)
objc_msgSend(selfClass,@selector(initialize));
}
接下來我來按照這個(gè)流程來解釋一下上面運(yùn)行的結(jié)果:
- 首先
PersonSub1
被使用,而此時(shí)PersonSub1
的isInitialized
為NO眷柔,而且父類Person
的isInitialized
也為NO期虾,所以先給父類發(fā)消息執(zhí)行initialize
,執(zhí)行完后Person
的isInitialized
變?yōu)閅ES驯嘱。然后PersonSub1
執(zhí)行自己的initialize
镶苞,執(zhí)行完后PersonSub1
的isInitialized
變?yōu)閅ES。所以這一步先打印+[Person initialize]
宙拉,然后打印+[PersonSub1 initialize]
宾尚。 - 然后是
Person
實(shí)例化,此時(shí)Person
的isInitialized
為YES,所以不會(huì)再調(diào)用initialize
煌贴。所以這一步什么都沒打印御板。 - 接著是
PersonSub2
實(shí)例化,此時(shí)PersonSub2
的isInitialized
為NO牛郑,父類Person
的isInitialized
為YES怠肋,所以只有PersonSub2
會(huì)執(zhí)行initialize
,執(zhí)行完后PersonSub2
的isInitialized
變?yōu)閅ES淹朋。所以這一步打印的是+[PersonSub2 initialize]
笙各。 - 再接著是
PersonSub3
實(shí)例化,此時(shí)PersonSub3
的isInitialized
為NO础芍,父類Person
的isInitialized
為YES杈抢,所以只有PersonSub3
會(huì)執(zhí)行initialize
,但是由于PersonSub3
沒有實(shí)現(xiàn)initialize
仑性,它就會(huì)去父類找這個(gè)方法的實(shí)現(xiàn)惶楼,找到后就執(zhí)行父類Person
的initialize
(注意這里是PersonSub3
執(zhí)行的Person
中的initialize
,而不是Person
執(zhí)行的)诊杆,執(zhí)行完后PersonSub3
的isInitialized
變?yōu)閅ES歼捐。所以這一步打印的是+[Person initialize]
竖伯。(注意這里打印的是方法信息铁孵,表示執(zhí)行的是Person
中的initialize
,而不是說是Person
調(diào)用的initialize
)坏匪。 - 最后是
PersonSub4
實(shí)例化淘这,這一步過程和上面一步是一樣的剥扣,執(zhí)行完后PersonSub4
的isInitialized
變?yōu)閅ES。這一步打印的是+[Person initialize]
慨灭。
所以最后的結(jié)果就是Person
朦乏、PersonSub1
、PersonSub2
氧骤、PersonSub3
呻疹、PersonSub4
這5個(gè)類都執(zhí)行了一次initialize
,雖然從運(yùn)行結(jié)果來看Person
的initialize
執(zhí)行了3次筹陵,其實(shí)后面2次是PersonSub3
和PersonSub4
調(diào)用的刽锤。
2.3 使用注意事項(xiàng)
雖然使用initialize
要比使用load
安全(因?yàn)樵谡{(diào)用initialize
時(shí)所有類已經(jīng)被加載進(jìn)內(nèi)存了),但我們還是要盡量少用initialize
這個(gè)方法個(gè)朦佩,尤其要謹(jǐn)慎在分類中實(shí)現(xiàn)initialize
方法并思,因?yàn)槿绻诜诸愔袑?shí)現(xiàn)了,本類實(shí)現(xiàn)的initialize
方法將不會(huì)被調(diào)用语稠。實(shí)際開發(fā)中initialize
方法一般用于初始化全局變量或靜態(tài)變量宋彼。