從runtime源代碼層面去研究下NSObject類初始化相關方法:load、initialize,以及在調用時内部做了什麼
目錄
一、load 方法
1. load_images
2. call_load_methods
二、initialize 方法
一、load 方法
+(void) load;
複制
作為iOS開發,多少都與 load 方法打過交道——在程式
main
函數調用前,類被注冊加載到記憶體時,
load
方法會被調用。也就是說每個類的
load
方法都會被調用一次。
在該方法中,我們最常用到的場景,就是使用 runtime 提供的交換函數
OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
,去改變系統方法行為并添加自定義的行為。
但若要了解 load 方法内部實作流程,還得從iOS程式啟動流程開始說起。
在程式的
main()
函數執行前,依次做了以下這些工作:
- 系統加載App自身所有的
(Mach-O檔案),并擷取可執行檔案
的路徑(dyld是專門用來加載動态連結庫的);dyld
-
初始化運作環境,并開啟dyld
(主要區分于App的冷啟動與熱啟動),從可執行檔案的依賴順序開始,遞歸加載所有依賴的動态連結庫,所有依賴庫通過dyld 緩存政策
内部實作将 Mach-O 檔案執行個體化為dyld
鏡像檔案。image
注:動态連結庫包括:所有系統 framework、系統級别的 libSystem(libdispatch、libsystem_blocks等)、加載 Objective-C runtime 的 libobjc(即Objective-C runtime 初始化)
- 當
對所有依賴庫初始化後,此時 runtime 會對項目中所有類進行類結構初始化,然後dyld
;調用所有類的 load 方法
- dyld最後會傳回
函數位址,main()
函數被調用,随後便進入熟悉的程式入口,預設從main()
類開始。AppDelegate
該章節僅僅是對 load 方法加載進行分析,是以關于 dyld 動态連結庫并不展開。
在一個類的 load 方法中添加斷點,編譯運作後,在控制台 lldb 中調用
bt
指令,可檢視到完整的堆棧調用資訊。
堆棧資訊中,在 dyld 加載完動态連結庫之後,類的
load
方法之前,runtime 調用了兩個函數:
load_images
與
call_load_methods
。
1. load_images
先來看下
load_images
第一步,會快速依次檢查類與分類中是否存在不帶鎖的 load 方法,這是在 runtime 中的注釋,講真的,不帶鎖的 load 方法,沒看懂。帶着好奇心去看一看
bool hasLoadMethods(const headerType *mhdr)
函數實作,發現了
_getObjc2NonlazyClassList
與
_getObjc2NonlazyCategoryList
。
程式初始化過程,所有 class 類實作都被存儲在 image 鏡像檔案中一個二進制清單裡,并且會在清單中擁有一個引用,這個二進制清單會允許 runtime 去追蹤檢索通路已存儲的類,但所有類并不會都在程式啟動時就要實作。是以當一個類實作 load 方法時,也會在這個二進制清單添加一個引用索引,讓 runtime 去追蹤通路。而這個二進制清單存儲于 image 鏡像檔案的 "__DATA, __objc_nlclslist, regular, no_dead_strip" 部分(看來後續文章要去深入了解dyld動态連結庫了)
在
_getObjc2NonlazyClassList
檢索類數組中已經實作
load
方法的類,也就是非懶加載類。非懶加載類一定會在程式啟動時實作 load 方法,與之對應的懶加載類卻并沒有實作。懶加載類會延遲到類第一次接收到消息時加載 load 方法。同理,
_getObjc2NonlazyCategoryList
作用于分類,與
_getObjc2NonlazyClassList
功能相同。
當檢索懶加載類時,則需要用到
_getObjc2ClassList
與
_getObjc2CategoryList
,分别檢索所有類(包括非懶加載類、懶加載類)、分類擴充(包括非懶加載類、懶加載類)。
是以,
bool hasLoadMethods(const headerType *mhdr)
函數作用,是查詢所有非懶加載類、類擴充數組中是否存在已加載 load 方法。但為什麼該函數在 runtime 中被注釋為:快速掃描不帶鎖的 load 方法。對于非懶加載類的 load 方法在 runtime 中被定義為不帶鎖的 load 方法?到現在還一直有這個疑問。
第二步,當判斷存在非懶加載類、類擴充的 load 方法時,會先用互斥鎖上鎖該線程,并執行
void prepare_load_methods(const headerType *mhdr)
函數。
在
void prepare_load_methods(const headerType *mhdr)
函數中,周遊
_getObjc2NonlazyClassList
函數裡已加載 load 方法的類,并先擷取目前處于活動狀态的類指針(因為類指針可能會指向已重新配置設定的類結構;并且會對 weak 連結的忽略,傳回 nil ),再遞歸去查找目前處于有效連接配接的類以及沒有調用 load 方法的父類,添加至可執行 load 方法加載數組中。而且,為了保證父類要在子類前調用 load 方法,是通過
static void schedule_class_load(Class cls)
函數遞歸來實作的。
最後通過
void add_class_to_loadable_list(Class cls)
函數,将已處于有效連接配接狀态的類添加至可加載
load
方法的類數組中,并且會将對應類的 load 方法 IMP 添加維護進一個專門維護 load 方法數組中。函數聲明也可以發現,通過遞歸讓類的超類先執行
void add_class_to_loadable_list(Class cls)
函數,當確定超類沒有實作 load 方法,就将超類添加至可加載 load 方法數組,随後再将該類添加至數組中。
當非懶加載類周遊添加至可執行 load 方法的類數組後,再對所有的分類也執行相同的操作,并将分類以及對應的方法 IMP 維護至對應數組中。但是在分類的周遊過程中,會首先對分類對應的類進行
static Class realizeClass(Class cls)
函數操作,将類進行初始化。關于
static Class realizeClass(Class cls)
函數的作用,前篇文章runtime的那些事(二)——NSObject資料結構已做介紹,為了能夠讓類對應的分類資訊加載至類結構體中,必須先要将類進行初始化。
當非懶加載類、分類資訊,以及對應 load 方法 IMP 準備完成後,接下來就會進入到
call_load_methods()
函數中。
2. call_load_methods
call_load_methods
函數聲明。
關于
call_load_methods
函數的作用,在 runtime 源碼已經給了很好的說明。
- 優先調用所有類的 load 方法,再去執行分類的 load 方法;
- 父類 load 方法優先于子類的執行;
- 該函數聲明是允許多次執行的,因為在 load 加載過程中會觸發更多的 image 鏡像檔案映射,而load 方法的調用是通過 dyld(動态連結庫)
,當每次有新的鏡像檔案添加時觸發(此處dyld的調用不展開);dyld_register_image_state_change_handler
- 通過 do while 循環一直重複去調用類 load 方法,直到可加載 load 方法的類不再有;
- 分類的 load 方法隻會執行一次,以確定“父類優先”的調用排序,即使分類加載時會觸發新的可加載類;
- 在 do while 循環執行 load 方法過程中,為了保證線程安全,
必須被調用者持有,其它任何鎖不能被持有。loadMethodLock
在 do while 循環外面,使用了 autoreleasePool 進行管理。每當循環執行完畢時,會及時清理中間過程産生的臨時變量以及記憶體資源消耗。
call_class_loads() 與 call_category_loads()
上述兩個方法分别是周遊調用類與分類 load 方法
call_class_loads()方法實作
在調用 load 方法時,并沒有通過
objc_msgSend()
方法來發送消息,而是直接擷取了對應類的 load 方法記憶體位址來調用
(*load_method)(cls, SEL_load);
,該調用方式最顯著的特性,就是類、父類、分類之間調用 load 方法不會互相影響,當實作了類的 load 方法時,不會主動調用父類的 load 方法。換句話說,也就是實作了類的 load 方法,不需調用
[super load];
方法。
而在
call_category_loads()
方法中,與
call_class_loads()
方法調用稍有不同。
- 先将可加載 load 方法的分類數組複制了一份相同結構體數組,命名為
;cats
- 在 cats 數組周遊加載分類 load 方法後(同樣是通過直接擷取 load 方法的記憶體位址來調用),會從 cats 中删除已加載 load 方法的分類;
- 再次檢查
數組中是否有新的可加載 load 方法的分類,若存在,先判斷分類數組記憶體是否已被全部占用,若全部占用則在目前數組記憶體的基礎上進行擴充,調用loadable_categories
進行動态配置設定記憶體修改,再将新的分類添加至 cats 中;realloc
- 銷毀原有的
loadable_categories
- 若不存在新的分類加入,則銷毀 cats 數組,
相關參數全部置為初始狀态,并 return NO,代表着全部分類已加載 load 方法完成;若存在新的分類加入 cats 數組,則會将數組 cats 指派給loadable_categories
,并在最後return YES,代表着有新的分類加入并需要加載其 load 方法。loadable_categories
小結
從 runtime 源碼層面去研究 load 方法的加載,從中也得到一些關于 load 方法的特性。
- 加載 load 方法是在程式初始化階段,runtime 初始化過程
中執行的;load_images
- 父類的 load 方法一定會優先于子類的 load 方法執行;
- 所有類的 load 方法執行在前,分類的 load 方法後續執行;
- 一個類即使不主動代碼調用 load 方法,其類、子類都會執行一次 load 方法;
- 不需要在 load 方法中調用
方法,内部會周遊遞歸向上查找父類并執行其 load 方法;[super load]
- 主工程中的類 load 方法加載是在 dyld 動态連結庫最後階段調用,意味着項目中引入的動态庫 load 方法會優先于主工程中的類 load 方法執行;
當然 load 方法還有一些其它特性,比如:
同一 image 鏡像檔案下,沒有關系的兩個類調用 load 方法的順序,是按照類檔案在 Compile Sources 中的順序執行;
同一 image 鏡像檔案下,每個類的分類若實作了 load 方法,都會去執行,執行順序也是按照分類檔案在 Compile Sources 中的順序;
二、initialize 方法
+(void) initialize;
複制
關于 initialize 方法的調用時機,什麼時候會調用 initialize 方法?
當引入一個類卻不對它做任何事的時候,并不會觸發 initialize 方法執行;隻有對該類進行第一次消息發送,即觸發調用
objc_msgSend()
方法時,才會去執行。
調用 initialize 方法
關于
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
,其作用是查找方法的實作 IMP,在類的消息發送流程中有着舉足輕重的地位。
在上述源碼中,當類第一次接收到消息時,會判斷出需要 initialize 方法初始化而且沒有執行過 initialize 方法,則會去執行
void _class_initialize(Class cls)
方法,并且對 initialize 方法執行加鎖保護。
在
void _class_initialize(Class cls)
方法中,首先會去遞歸檢查父類是否已經執行過 initialize 方法。
然後,判斷目前類的 flags 掩碼位運算不是
RW_INITIALIZED
與
RW_INITIALIZING
時,設定其 flags 掩碼位為
RW_INITIALIZING
,标記為需要執行 initialize 方法。并使用原子保護,防止重複執行 initialize 方法。
最後,去執行
callInitialize(cls);
方法,而這個方法的實作也非常簡單,
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
。差別于 load 方法的執行,使用
objc_msgSend()
消息發送執行
SEL_initialize
selector,并沒有像 load 方法一樣直接擷取 selector 的記憶體位址來調用。既然是使用了
objc_msgSend
走消息發送流程,當子類沒有實作時,會調用繼承的父類實作;若分類實作了 initialize 方法,那麼就會優先執行分類的(本類中的 initialize 方法實作并沒有被覆寫,依然存在與類資訊中,隻是因為分類實作了并優先執行分類的 initialize 方法)
小結
- initialize 在類第一次接收到消息時調用,也就是
,其本質也是通過objc_msgSend()
方法調用;objc_msgSend
- 在類初始化過程中,會優先調用父類的 initialize,再調用本類的 initialize;
-
若本類沒有實作 initialize,而父類實作了 initialize ,那麼本類的初始化會去調用并繼承父類的 initialize 方法,通過
superclass 到父類中查找,意味着父類的 initialize 方法可能會多次調用;
- 本類的 initialize 方法實作會覆寫之前繼承自父類的 initialize 方法;
- 在重寫 initialize 方法時,不需要調用
方法,因為其内部會自動遞歸向上查找執行父類 initialize 方法;[super initialize]
- 分類中的 initialize 方法會優先執行,本類中的 initialize 方法不會再調用,究其原因是
方法機制;obj_msgSend
關于 initialize 的一些其它特性:
當有多個分類實作了 initialize 方法時,隻會執行最後一個分類的(最後一個是指在 Compile Sources 中排列順序最靠後的分類);
後記:
關于類的初始化 load 與 initialize 方法就先寫到這裡。在整理寫作過程中,我自己也發現了有很多還需要待完善的知識點,比如:每個類、分類 load 方法是何時、如何加載進可加載 load 清單中,dyld 動态連結庫對 image 鏡像檔案的操作流程。後續會不斷補充,若是文章中出現不準确的地方還請多多指點。
該文章首次發表在 簡書:我隻不過是出來寫寫代碼 部落格,并自動同步至 騰訊雲:我隻不過是出來寫寫iOS 部落格