天天看點

runtime的那些事(三)——NSObject初始化 load 與 initialize

從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()

函數執行前,依次做了以下這些工作:

  1. 系統加載App自身所有的

    可執行檔案

    (Mach-O檔案),并擷取

    dyld

    的路徑(dyld是專門用來加載動态連結庫的);
  2. dyld

    初始化運作環境,并開啟

    dyld 緩存政策

    (主要區分于App的冷啟動與熱啟動),從可執行檔案的依賴順序開始,遞歸加載所有依賴的動态連結庫,所有依賴庫通過

    dyld

    内部實作将 Mach-O 檔案執行個體化為

    image

    鏡像檔案。
注:動态連結庫包括:所有系統 framework、系統級别的 libSystem(libdispatch、libsystem_blocks等)、加載 Objective-C runtime 的 libobjc(即Objective-C runtime 初始化)
  1. dyld

    對所有依賴庫初始化後,此時 runtime 會對項目中所有類進行類結構初始化,然後

    調用所有類的 load 方法

  2. dyld最後會傳回

    main()

    函數位址,

    main()

    函數被調用,随後便進入熟悉的程式入口,預設從

    AppDelegate

    類開始。

 該章節僅僅是對 load 方法加載進行分析,是以關于 dyld 動态連結庫并不展開。

 在一個類的 load 方法中添加斷點,編譯運作後,在控制台 lldb 中調用

bt

指令,可檢視到完整的堆棧調用資訊。

runtime的那些事(三)——NSObject初始化 load 與 initialize

堆棧資訊中,在 dyld 加載完動态連結庫之後,類的

load

方法之前,runtime 調用了兩個函數:

load_images

call_load_methods

1. load_images

先來看下

load_images

runtime的那些事(三)——NSObject初始化 load 與 initialize

 第一步,會快速依次檢查類與分類中是否存在不帶鎖的 load 方法,這是在 runtime 中的注釋,講真的,不帶鎖的 load 方法,沒看懂。帶着好奇心去看一看

bool hasLoadMethods(const headerType *mhdr)

函數實作,發現了

_getObjc2NonlazyClassList

_getObjc2NonlazyCategoryList

runtime的那些事(三)——NSObject初始化 load 與 initialize

程式初始化過程,所有 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)

函數。

runtime的那些事(三)——NSObject初始化 load 與 initialize

 在

void prepare_load_methods(const headerType *mhdr)

函數中,周遊

_getObjc2NonlazyClassList

函數裡已加載 load 方法的類,并先擷取目前處于活動狀态的類指針(因為類指針可能會指向已重新配置設定的類結構;并且會對 weak 連結的忽略,傳回 nil ),再遞歸去查找目前處于有效連接配接的類以及沒有調用 load 方法的父類,添加至可執行 load 方法加載數組中。而且,為了保證父類要在子類前調用 load 方法,是通過

static void schedule_class_load(Class cls)

函數遞歸來實作的。

runtime的那些事(三)——NSObject初始化 load 與 initialize

 最後通過

void add_class_to_loadable_list(Class cls)

函數,将已處于有效連接配接狀态的類添加至可加載

load

方法的類數組中,并且會将對應類的 load 方法 IMP 添加維護進一個專門維護 load 方法數組中。函數聲明也可以發現,通過遞歸讓類的超類先執行

void add_class_to_loadable_list(Class cls)

函數,當確定超類沒有實作 load 方法,就将超類添加至可加載 load 方法數組,随後再将該類添加至數組中。

runtime的那些事(三)——NSObject初始化 load 與 initialize

 當非懶加載類周遊添加至可執行 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

函數聲明。

runtime的那些事(三)——NSObject初始化 load 與 initialize

關于

call_load_methods

函數的作用,在 runtime 源碼已經給了很好的說明。

  1. 優先調用所有類的 load 方法,再去執行分類的 load 方法;
  2. 父類 load 方法優先于子類的執行;
  3. 該函數聲明是允許多次執行的,因為在 load 加載過程中會觸發更多的 image 鏡像檔案映射,而load 方法的調用是通過 dyld(動态連結庫)

    dyld_register_image_state_change_handler

    ,當每次有新的鏡像檔案添加時觸發(此處dyld的調用不展開);
  4. 通過 do while 循環一直重複去調用類 load 方法,直到可加載 load 方法的類不再有;
  5. 分類的 load 方法隻會執行一次,以確定“父類優先”的調用排序,即使分類加載時會觸發新的可加載類;
  6. 在 do while 循環執行 load 方法過程中,為了保證線程安全,

    loadMethodLock

    必須被調用者持有,其它任何鎖不能被持有。

在 do while 循環外面,使用了 autoreleasePool 進行管理。每當循環執行完畢時,會及時清理中間過程産生的臨時變量以及記憶體資源消耗。

call_class_loads() 與 call_category_loads()

上述兩個方法分别是周遊調用類與分類 load 方法

runtime的那些事(三)——NSObject初始化 load 與 initialize

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 方法的分類;
  • 再次檢查

    loadable_categories

    數組中是否有新的可加載 load 方法的分類,若存在,先判斷分類數組記憶體是否已被全部占用,若全部占用則在目前數組記憶體的基礎上進行擴充,調用

    realloc

    進行動态配置設定記憶體修改,再将新的分類添加至 cats 中;
  • 銷毀原有的

    loadable_categories

  • 若不存在新的分類加入,則銷毀 cats 數組,

    loadable_categories

    相關參數全部置為初始狀态,并 return NO,代表着全部分類已加載 load 方法完成;若存在新的分類加入 cats 數組,則會将數組 cats 指派給

    loadable_categories

    ,并在最後return YES,代表着有新的分類加入并需要加載其 load 方法。

小結

從 runtime 源碼層面去研究 load 方法的加載,從中也得到一些關于 load 方法的特性。

  1. 加載 load 方法是在程式初始化階段,runtime 初始化過程

    load_images

    中執行的;
  2. 父類的 load 方法一定會優先于子類的 load 方法執行;
  3. 所有類的 load 方法執行在前,分類的 load 方法後續執行;
  4. 一個類即使不主動代碼調用 load 方法,其類、子類都會執行一次 load 方法;
  5. 不需要在 load 方法中調用

    [super load]

    方法,内部會周遊遞歸向上查找父類并執行其 load 方法;
  6. 主工程中的類 load 方法加載是在 dyld 動态連結庫最後階段調用,意味着項目中引入的動态庫 load 方法會優先于主工程中的類 load 方法執行;

當然 load 方法還有一些其它特性,比如:

同一 image 鏡像檔案下,沒有關系的兩個類調用 load 方法的順序,是按照類檔案在 Compile Sources 中的順序執行;

同一 image 鏡像檔案下,每個類的分類若實作了 load 方法,都會去執行,執行順序也是按照分類檔案在 Compile Sources 中的順序;

二、initialize 方法

+(void) initialize;           

複制

關于 initialize 方法的調用時機,什麼時候會調用 initialize 方法?

 當引入一個類卻不對它做任何事的時候,并不會觸發 initialize 方法執行;隻有對該類進行第一次消息發送,即觸發調用

objc_msgSend()

方法時,才會去執行。

runtime的那些事(三)——NSObject初始化 load 與 initialize

調用 initialize 方法

關于

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)

,其作用是查找方法的實作 IMP,在類的消息發送流程中有着舉足輕重的地位。

在上述源碼中,當類第一次接收到消息時,會判斷出需要 initialize 方法初始化而且沒有執行過 initialize 方法,則會去執行

void _class_initialize(Class cls)

方法,并且對 initialize 方法執行加鎖保護。

runtime的那些事(三)——NSObject初始化 load 與 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 方法)

小結

  1. initialize 在類第一次接收到消息時調用,也就是

    objc_msgSend()

    ,其本質也是通過

    objc_msgSend

    方法調用;
  2. 在類初始化過程中,會優先調用父類的 initialize,再調用本類的 initialize;
  3. 若本類沒有實作 initialize,而父類實作了 initialize ,那麼本類的初始化會去調用并繼承父類的 initialize 方法,通過

    superclass 到父類中查找,意味着父類的 initialize 方法可能會多次調用;

  4. 本類的 initialize 方法實作會覆寫之前繼承自父類的 initialize 方法;
  5. 在重寫 initialize 方法時,不需要調用

    [super initialize]

    方法,因為其内部會自動遞歸向上查找執行父類 initialize 方法;
  6. 分類中的 initialize 方法會優先執行,本類中的 initialize 方法不會再調用,究其原因是

    obj_msgSend

    方法機制;

關于 initialize 的一些其它特性:

當有多個分類實作了 initialize 方法時,隻會執行最後一個分類的(最後一個是指在 Compile Sources 中排列順序最靠後的分類);

後記:

 關于類的初始化 load 與 initialize 方法就先寫到這裡。在整理寫作過程中,我自己也發現了有很多還需要待完善的知識點,比如:每個類、分類 load 方法是何時、如何加載進可加載 load 清單中,dyld 動态連結庫對 image 鏡像檔案的操作流程。後續會不斷補充,若是文章中出現不準确的地方還請多多指點。

該文章首次發表在 簡書:我隻不過是出來寫寫代碼 部落格,并自動同步至 騰訊雲:我隻不過是出來寫寫iOS 部落格