load與initialize方法總結(jié):
load:
1、不走objc_msgSend流程侵佃,根據(jù)load方法的地址直接調(diào)用麻昼,并且在執(zhí)行main函數(shù)之前調(diào)用。
2馋辈、按編譯順序依次遍歷類抚芦,看當(dāng)前類是否實現(xiàn)了load方法,如果實現(xiàn)了首有,那么在此之前會先看當(dāng)前類父類有沒有實現(xiàn)load方法燕垃,有則父類的load方法先調(diào)用。所有類的load方法執(zhí)行完畢后井联,按照編譯順序執(zhí)行所有分類的load方法卜壕。注意:我們通常在load方法中進(jìn)行方法交換(Method Swizzle),除此之外烙常,除非真的有必要轴捎,我們盡量不要在load方法中寫代碼鹤盒,尤其不要在load方法中使用其它的類,因為這個時候其它的類可能還沒有被加載進(jìn)內(nèi)存侦副,隨意使用可能會出問題侦锯。如果確實要在load方法寫一些代碼,那也要盡量精簡代碼秦驯,不要做一些耗時或者等待鎖的操作尺碰,因為整個程序在執(zhí)行l(wèi)oad方法時都會阻塞,從而導(dǎo)致程序啟動時間過長甚至無法啟動译隘。
initialize:
1亲桥、與普通方法調(diào)用一樣,走的是objc_msgSend流程固耘,類或它的子類收到第一條消息時會調(diào)用题篷,這里的消息就是指實例方法或類方法的調(diào)用,所以所有類的initialize調(diào)用是在執(zhí)行main函數(shù)之后調(diào)用的厅目。
2番枚、如果一個類和它的分類都實現(xiàn)了initialize方法,那最終調(diào)用的會是分類中的方法损敷。如果子類和父類都實現(xiàn)了initialize方法葫笼,那么會先調(diào)用父類的方法,然后調(diào)用子類的方法(這里注意子類中不需要顯示調(diào)用父類的方法拗馒,通過查看源碼得知它是在底層實現(xiàn)過程中主動調(diào)用的父類的initialize方法)渔欢。
3、一個類只會調(diào)用一次initialize方法瘟忱,并且未被使用則不會調(diào)用。注意:雖然使用initialize要比使用load安全(因為在調(diào)用initialize時所有類已經(jīng)被加載進(jìn)內(nèi)存了)苫幢,但我們還是要盡量少用initialize這個方法個访诱,尤其要謹(jǐn)慎在分類中實現(xiàn)initialize方法,因為如果在分類中實現(xiàn)了韩肝,本類實現(xiàn)的initialize方法將不會被調(diào)用触菜。實際開發(fā)中initialize方法一般用于初始化全局變量或靜態(tài)變量。
load與initialize方法
OC文件在編譯后哀峻,類相關(guān)的數(shù)據(jù)結(jié)構(gòu)會保留在目標(biāo)文件中涡相,在運行時得到解析和使用。在應(yīng)用程序運行起來的時候剩蟀,類的信息會有加載和初始化過程催蝗,這個過程就涉及到了類的兩個類方法:load
和initialize
。下面我們就來介紹一下這2個方法的區(qū)別育特。(首先要說明一下丙号,這2個方法是系統(tǒng)調(diào)用的,開發(fā)者一般不會主動去調(diào)用者兩個方法,這么做也沒有什么意義犬缨,所以后面的講解都是針對系統(tǒng)調(diào)用喳魏,不考慮主動調(diào)用的情況)。
1. load方法
1.1 調(diào)用時機(jī)
當(dāng)我們啟動程序時怀薛,參與了編譯的類刺彩、分類都會被加載進(jìn)內(nèi)存,load
方法就是在這個類被加載的時候調(diào)用的(前提是這個類有實現(xiàn)load
方法)枝恋,這個過程與這個類是否被使用是無關(guān)的创倔,也就是說如果有一個類(MyClass)即使在整個程序中都沒有用到,甚至沒有任何一個文件去引入MyClass的頭文件鼓择,MyClass的的load
的方法一樣會被調(diào)用三幻。等所有的類、分類都加載進(jìn)內(nèi)存后才會調(diào)用程序的main
函數(shù)呐能,所以所有類的load
方法都是在main
函數(shù)之前被調(diào)用的念搬。而且每個類、分類的load
方法只會被調(diào)用一次摆出。
1.2 調(diào)用順序
一個程序中如果所有的類朗徊、分類都實現(xiàn)了load
方法,那么所有的load
方法都會被調(diào)用偎漫。它們的執(zhí)行順序遵循以下規(guī)則:
先執(zhí)行所有類的
load
方法爷恳,再執(zhí)行所有分類的load
方法。執(zhí)行類的
load
方法時象踊,是按照參與編譯的順序温亲,先編譯的類先執(zhí)行,但是如果某個類是繼承自另一個類杯矩,那么會先執(zhí)行父類的load
方法個再執(zhí)行自己的load
方法栈虚。執(zhí)行分類的
load
方法時,是按照分類參與編譯的順序史隆,先編譯的分類先執(zhí)行魂务。
關(guān)于編譯順序,我們可以在項目的Build Phases
--> Compile Sources
查看泌射,最上面的就最先編譯粘姜,我們可以拖動文件來調(diào)整編譯順序。
下面舉個例子來看下load
方法的執(zhí)行順序熔酷。首先說明一下幾個類的關(guān)系:Person
類有aaa
和bbb
兩個分類孤紧,men
類繼承自Person
類,men
也有2個分類ccc
和ddd
纯陨,Book
類和前面這些類沒有任何關(guān)系坛芽。
解釋:
編譯順序從上到下留储,上面先編譯,下面后編譯咙轩。由于先執(zhí)行類的
load
再執(zhí)行分類的load
获讳,最先參與編譯的類是men
,而men
繼承自Person
活喊,所以最先執(zhí)行Person
的load
(雖然Person
是后參與編譯的丐膝,但是它是父類,所以會先執(zhí)行)钾菊,然后再執(zhí)行men
的load
吧史。接著參與編譯的是Book
類观游,所以緊接著就是執(zhí)行Book
的load
囤萤。再接著參與編譯的類就是Person
猴鲫,由于它的load
方法已經(jīng)執(zhí)行過了,此時就不會執(zhí)行了滞详。所有的類的
load
方法都執(zhí)行完后開始執(zhí)行分類的load
凛俱,分類參與編譯的順序是men+ccc
-->Person+aaa
-->men+ddd
-->Person+bbb
,所以分類的load
方法個也是按照這個順序執(zhí)行料饥。
1.3 執(zhí)行方式
我們知道蒲犬,當(dāng)分類中存在和本類中同名的方法時,調(diào)用這個方法最終執(zhí)行的是分類中的方法岸啡。那上面就很奇怪了原叮,Person
和Person的分類
中都有load
方法,按理說調(diào)用load
方法時最終只會調(diào)用其中一個分類的load
方法巡蘸,可結(jié)果Person
本類和它的2個分類都調(diào)用了load
方法奋隶。
這是因為load
方法和普通方法調(diào)用的方式不一樣。普通方法調(diào)用是通過消息發(fā)送機(jī)制實現(xiàn)的悦荒,會先去類或元類的方法列表中查找达布,如果找到了方法就執(zhí)行,如果沒有找到就去父類的方法列表里面找逾冬,只要找到就會終止查找,所以只會執(zhí)行一次躺苦。
而load
方法調(diào)用時身腻,每個類都是根據(jù)load
方法的地址直接調(diào)用,而不會走objc_msgSend
函數(shù)的方法查找流程匹厘,也就是說一個類有實現(xiàn)load
方法就執(zhí)行嘀趟,沒有就不執(zhí)行(沒有的話也不會去父類里面查找)。
想要了解更加詳細(xì)的底層實現(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 實現(xiàn)load方法時要注意什么
我們通常在load
方法中進(jìn)行方法交換(Method Swizzle),除此之外酌泰,除非真的有必要媒佣,我們盡量不要在load
方法中寫代碼,尤其不要在load
方法中使用其它的類陵刹,因為這個時候其它的類可能還沒有被加載進(jìn)內(nèi)存默伍,隨意使用可能會出問題。
如果確實要在load
方法寫一些代碼衰琐,那也要盡量精簡代碼也糊,不要做一些耗時或者等待鎖的操作,因為整個程序在執(zhí)行load
方法時都會阻塞羡宙,從而導(dǎo)致程序啟動時間過長甚至無法啟動狸剃。
2. initialize方法
2.1 調(diào)用時機(jī)
initialize
方法是在類或它的子類收到第一條消息時被調(diào)用的,這里的消息就是指實例方法或類方法的調(diào)用狗热,所以所有類的initialize
調(diào)用是在執(zhí)行main
函數(shù)之后調(diào)用的钞馁。而且一個類只會調(diào)用一次initialize
方法。如果一個類在程序運行過程中一直沒有被使用過斗搞,那這個類的initialize
方法也就不會被調(diào)用指攒,這一點和load
方法是不一樣的。
2.2 調(diào)用方式
initialize
方法的調(diào)用和普通方法調(diào)用一樣僻焚,也是走的objc_msgSend
流程允悦。所以如果一個類和它的分類都實現(xiàn)了initialize
方法,那最終調(diào)用的會是分類中的方法虑啤。
如果子類和父類都實現(xiàn)了initialize
方法隙弛,那么會先調(diào)用父類的方法,然后調(diào)用子類的方法(這里注意子類中不需要寫[super initialize]
來調(diào)用父類的方法狞山,通過查看源碼得知它是在底層實現(xiàn)過程中主動調(diào)用的父類的initialize
方法)全闷。
下面看一個例子:
父類Person
實現(xiàn)了initialize
,PersonSub1
和PersonSub2
這兩個子類也實現(xiàn)了initialize
萍启,PersonSub3
和PersonSub4
這兩個子類沒有實現(xiàn)了initialize
总珠,按照下面的順序?qū)嵗瘜ο螅?/p>
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]
看到這個運行結(jié)果,有人就有疑問了:不是說一個類只會調(diào)用一次initialize
方法嗎勘纯,為什么這里Person
的initialize
方法被調(diào)用了3次局服?
這里就需要講解一下底層源碼的執(zhí)行流程了,每個類都有一個標(biāo)記記錄這個類是否調(diào)用過initialize
驳遵,我這里就用一個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));
}
復(fù)制代碼
接下來我來按照這個流程來解釋一下上面運行的結(jié)果:
首先
PersonSub1
被使用唆迁,而此時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
實例化肴盏,此時Person
的isInitialized
為YES科盛,所以不會再調(diào)用initialize
。所以這一步什么都沒打印菜皂。接著是
PersonSub2
實例化贞绵,此時PersonSub2
的isInitialized
為NO,父類Person
的isInitialized
為YES恍飘,所以只有PersonSub2
會執(zhí)行initialize
榨崩,執(zhí)行完后PersonSub2
的isInitialized
變?yōu)閅ES。所以這一步打印的是+[PersonSub2 initialize]
章母。再接著是
PersonSub3
實例化母蛛,此時PersonSub3
的isInitialized
為NO,父類Person
的isInitialized
為YES乳怎,所以只有PersonSub3
會執(zhí)行initialize
彩郊,但是由于PersonSub3
沒有實現(xiàn)initialize
,它就會去父類找這個方法的實現(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
實例化,這一步過程和上面一步是一樣的渊抄,執(zhí)行完后PersonSub4
的isInitialized
變?yōu)閅ES惠险。這一步打印的是+[Person initialize]
。
所以最后的結(jié)果就是Person
抒线、PersonSub1
、PersonSub2
渣慕、PersonSub3
嘶炭、PersonSub4
這5個類都執(zhí)行了一次initialize
抱慌,雖然從運行結(jié)果來看Person
的initialize
執(zhí)行了3次,其實后面2次是PersonSub3
和PersonSub4
調(diào)用的眨猎。
2.3 使用注意事項
雖然使用initialize
要比使用load
安全(因為在調(diào)用initialize
時所有類已經(jīng)被加載進(jìn)內(nèi)存了)抑进,但我們還是要盡量少用initialize
這個方法個,尤其要謹(jǐn)慎在分類中實現(xiàn)initialize
方法睡陪,因為如果在分類中實現(xiàn)了寺渗,本類實現(xiàn)的initialize
方法將不會被調(diào)用。實際開發(fā)中initialize
方法一般用于初始化全局變量或靜態(tài)變量兰迫。