OC文件在編譯後,類相關的數據結構會保留在目標文件中,在運行時獲得解析和使用。在應用程序運行起來的時候,類的信息會有加載和初始化過程,這個過程就涉及到了類的兩個類方法:load
和initialize
。下面咱們就來介紹一下這2個方法的區別。(首先要說明一下,這2個方法是系統調用的,開發者通常不會主動去調用者兩個方法,這麼作也沒有什麼意義,因此後面的講解都是針對系統調用,不考慮主動調用的狀況)。安全
當咱們啓動程序時,參與了編譯的類、分類都會被加載進內存,load
方法就是在這個類被加載的時候調用的(前提是這個類有實現load
方法),這個過程與這個類是否被使用是無關的,也就是說若是有一個類(MyClass)即便在整個程序中都沒有用到,甚至沒有任何一個文件去引入MyClass的頭文件,MyClass的的load
的方法同樣會被調用。等全部的類、分類都加載進內存後纔會調用程序的main
函數,因此全部類的load
方法都是在main
函數以前被調用的。並且每一個類、分類的load
方法只會被調用一次。bash
一個程序中若是全部的類、分類都實現了load
方法,那麼全部的load
方法都會被調用。它們的執行順序遵循如下規則:數據結構
load
方法,再執行全部分類的load
方法。load
方法時,是按照參與編譯的順序,先編譯的類先執行,可是若是某個類是繼承自另外一個類,那麼會先執行父類的load
方法個再執行本身的load
方法。load
方法時,是按照分類參與編譯的順序,先編譯的分類先執行。關於編譯順序,咱們能夠在項目的Build Phases
--> Compile Sources
查看,最上面的就最早編譯,咱們能夠拖動文件來調整編譯順序。app
下面舉個例子來看下load
方法的執行順序。首先說明一下幾個類的關係:Person
類有aaa
和bbb
兩個分類,men
類繼承自Person
類,men
也有2個分類ccc
和ddd
,Book
類和前面這些類沒有任何關係。函數
load
再執行分類的load
,最早參與編譯的類是men
,而men
繼承自Person
,因此最早執行Person
的load
(雖然Person
是後參與編譯的,可是它是父類,因此會先執行),而後再執行men
的load
。接着參與編譯的是Book
類,因此緊接着就是執行Book
的load
。再接着參與編譯的類就是Person
,因爲它的load
方法已經執行過了,此時就不會執行了。load
方法都執行完後開始執行分類的load
,分類參與編譯的順序是men+ccc
-->Person+aaa
-->men+ddd
-->Person+bbb
,因此分類的load
方法個也是按照這個順序執行。咱們知道,當分類中存在和本類中同名的方法時,調用這個方法最終執行的是分類中的方法。那上面就很奇怪了,Person
和Person的分類
中都有load
方法,按理說調用load
方法時最終只會調用其中一個分類的load
方法,可結果Person
本類和它的2個分類都調用了load
方法。ui
這是由於load
方法和普通方法調用的方式不同。普通方法調用是經過消息發送機制實現的,會先去類或元類的方法列表中查找,若是找到了方法就執行,若是沒有找到就去父類的方法列表裏面找,只要找到就會終止查找,因此只會執行一次。spa
而load
方法調用時,每一個類都是根據load
方法的地址直接調用,而不會走objc_msgSend
函數的方法查找流程,也就是說一個類有實現load
方法就執行,沒有就不執行(沒有的話也不會去父類裏面查找)。code
想要了解更加詳細的底層實現流程,能夠去看objc4源碼,這裏提供一下相關函數調用流程以便進行源碼閱讀: 首先從objc-os.mm
文件的_objc_init
函數開始-->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)
。cdn
咱們一般在load
方法中進行方法交換(Method Swizzle),除此以外,除非真的有必要,咱們儘可能不要在load
方法中寫代碼,尤爲不要在load
方法中使用其它的類,由於這個時候其它的類可能尚未被加載進內存,隨意使用可能會出問題。對象
若是確實要在load
方法寫一些代碼,那也要儘可能精簡代碼,不要作一些耗時或者等待鎖的操做,由於整個程序在執行load
方法時都會阻塞,從而致使程序啓動時間過長甚至沒法啓動。
initialize
方法是在類或它的子類收到第一條消息時被調用的,這裏的消息就是指實例方法或類方法的調用,因此全部類的initialize
調用是在執行main
函數以後調用的。並且一個類只會調用一次initialize
方法。若是一個類在程序運行過程當中一直沒有被使用過,那這個類的initialize
方法也就不會被調用,這一點和load
方法是不同的。
initialize
方法的調用和普通方法調用同樣,也是走的objc_msgSend
流程。因此若是一個類和它的分類都實現了initialize
方法,那最終調用的會是分類中的方法。
若是子類和父類都實現了initialize
方法,那麼會先調用父類的方法,而後調用子類的方法個(這裏注意子類中不須要寫[super initialize]
來調用父類的方法,經過查看源碼得知它是在底層實現過程當中主動調用的父類的initialize
方法)。
下面看一個例子:
父類Person
實現了initialize
,PersonSub1
和PersonSub2
這兩個子類也實現了initialize
,PersonSub3
和PersonSub4
這兩個子類沒有實現了initialize
,按照下面的順序實例化對象:
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];
// ***************打印結果***************
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]
複製代碼
看到這個運行結果,有人就有疑問了:不是說一個類只會調用一次initialize
方法嗎,爲何這裏Person
的initialize
方法被調用了3次?
這裏就須要講解一下底層源碼的執行流程了,每一個類都有一個標記記錄這個類是否調用過initialize
,我這裏就用一個BOOL類型的isInitialized
來表示,而後用selfClass
來表示本身的類,用superClass
來表示父類,下面我用僞代碼來描述一下底層源碼執行流程:
// 若是本身沒有調用過initialize就執行裏面的代碼
if(!selfClass.isInitialized){
if(!superClass.isInitialized){
// 若是父類沒有執行過initialize就給父類發消息(一旦成功執行initialize就將父類的isInitialized置爲YES)
objc_msgSend(superClass,@selector(initialize));
}
// 再給本身的類發消息(一旦成功執行initialize就將本身類的isInitialized置爲YES)
objc_msgSend(selfClass,@selector(initialize));
}
複製代碼
接下來我來按照這個流程來解釋一下上面運行的結果:
PersonSub1
被使用,而此時PersonSub1
的isInitialized
爲NO,並且父類Person
的isInitialized
也爲NO,因此先給父類發消息執行initialize
,執行完後Person
的isInitialized
變爲YES。而後PersonSub1
執行本身的initialize
,執行完後PersonSub1
的isInitialized
變爲YES。因此這一步先打印+[Person initialize]
,而後打印+[PersonSub1 initialize]
。Person
實例化,此時Person
的isInitialized
爲YES,因此不會再調用initialize
。因此這一步什麼都沒打印。PersonSub2
實例化,此時PersonSub2
的isInitialized
爲NO,父類Person
的isInitialized
爲YES,因此只有PersonSub2
會執行initialize
,執行完後PersonSub2
的isInitialized
變爲YES。因此這一步打印的是+[PersonSub2 initialize]
。PersonSub3
實例化,此時PersonSub3
的isInitialized
爲NO,父類Person
的isInitialized
爲YES,因此只有PersonSub3
會執行initialize
,可是因爲PersonSub3
沒有實現initialize
,它就會去父類找這個方法的實現,找到後就執行父類Person
的initialize
(注意這裏是PersonSub3
執行的Person
中的initialize
,而不是Person
執行的),執行完後PersonSub3
的isInitialized
變爲YES。因此這一步打印的是+[Person initialize]
。(注意這裏打印的是方法信息,表示執行的是Person
中的initialize
,而不是說是Person
調用的initialize
)。PersonSub4
實例化,這一步過程和上面一步是同樣的,執行完後PersonSub4
的isInitialized
變爲YES。這一步打印的是+[Person initialize]
。因此最後的結果就是Person
、PersonSub1
、PersonSub2
、PersonSub3
、PersonSub4
這5個類都執行了一次initialize
,雖然從運行結果來看Person
的initialize
執行了3次,其實後面2次是PersonSub3
和PersonSub4
調用的。
雖然使用initialize
要比使用load
安全(由於在調用initialize
時全部類已經被加載進內存了),但咱們仍是要儘可能少用initialize
這個方法個,尤爲要謹慎在分類中實現initialize
方法,由於若是在分類中實現了,本類實現的initialize
方法將不會被調用。實際開發中initialize
方法通常用於初始化全局變量或靜態變量。