一個 iOS App 的 main 函數(shù)位于 main.m 中,這是我們熟知的程序入口。但對 objc 了解更多之后發(fā)現(xiàn),程序在進(jìn)入我們的 main 函數(shù)前已經(jīng)執(zhí)行了很多代碼,比如熟知的動態(tài)庫的加載, runtime 的初始化, + load 方法等。本文將跟隨程序執(zhí)行順序,刨根問底,從 dyld 到 runtime ,看看程序從啟動到 main 函數(shù)執(zhí)行之前都發(fā)生了什么。
main之前的加載過程
- Mach-O 加載
- 從 dyld 開始
- 加載程序相關(guān)依賴庫, 并對這些庫進(jìn)行鏈接;
- 調(diào)用每個依賴庫的初始化方法,在這一步,runtime被初始化;
- 將程序依賴的動態(tài)鏈接庫遞歸加載進(jìn)內(nèi)存;
- runtime會對項(xiàng)目中所有類進(jìn)行類結(jié)構(gòu)初始化,然后調(diào)用所有的load方法;
- dyld返回main函數(shù)地址,main函數(shù)被調(diào)用;
- main函數(shù)
Mach-O 加載
關(guān)于 Mach-O 文件這里 有一篇 文章介紹 Mach-O 文件.
Mach-O文件格式是 OS X 與 iOS 系統(tǒng)上的可執(zhí)行文件格式,像我們編譯過程產(chǎn)生的 .O 文件,以及程序的可執(zhí)行文件,動態(tài)庫等都是Mach-O文件。它的結(jié)構(gòu)如下:
有如下幾個部分組成:
Header:保存了一些基本信息,包括了該文件運(yùn)行的平臺、文件類型、LoadCommands的個數(shù)等等。
LoadCommands:可以理解為加載命令,在加載Mach-O文件時會使用這里的數(shù)據(jù)來確定內(nèi)存的分布以及相關(guān)的加載命令。比如我們的main函數(shù)的加載地址,程序所需的dyld的文件路徑,以及相關(guān)依賴庫的文件路徑。
Data: 這里包含了具體的代碼、數(shù)據(jù)等等。
系統(tǒng)加載程序可執(zhí)行文件后,通過分析文件來獲得dyld所在路徑來加載dyld,然后就將后面的事情甩給 dyld 了。
從 dyld 開始
-
dyld: (the dynamic link editor)動態(tài)鏈接器,其源碼是開源的。
dyld::_main函數(shù)源碼
一切源于dyldStartup.s
這個文件,其中用匯編實(shí)現(xiàn)了名為__dyld_start
的方法,匯編太生澀,它主要干了兩件事:
調(diào)用 dyldbootstrap::start()
方法(省去參數(shù))
上個方法返回了main
函數(shù)地址,填入?yún)?shù)并調(diào)用main
函數(shù)
這個步驟隨手就能驗(yàn)證出來,設(shè)置一個符號斷點(diǎn)斷在_objc_init:
這個函數(shù)是runtime的初始化函數(shù),后面會提到。程序運(yùn)行在很早的時候斷住,這時候看調(diào)用棧:
看到了棧底的
dyldbootstrap::start()
方法,繼而調(diào)用了 dyld::_main()
方法,其中完成了剛才說的遞歸加載動態(tài)庫過程,由于 libSystem
默認(rèn)引入,棧中出現(xiàn)了 libSystem_initializer
的初始化方法。
- ImageLoader: 用于輔助加載特定可執(zhí)行文件格式的類,程序中對應(yīng)實(shí)例可簡稱為image(如程序可執(zhí)行文件,F(xiàn)ramework庫,bundle文件)。
兩步走:
- 在程序運(yùn)行時它先將動態(tài)鏈接的 image 遞歸加載 (也就是上面測試棧中一串的遞歸調(diào)用的時刻);
- 再從可執(zhí)行文件 image 遞歸加載所有符號;
當(dāng)然所有這些都發(fā)生在我們真正的 main 函數(shù)執(zhí)行前。
-
runtime 與 +load
剛才說到libSystem
是若干個系統(tǒng) lib 的集合,所以它只是一個容器 lib 而已,而且它也是開源的,里面實(shí)質(zhì)上就一個文件,init.c,由libSystem_initializer
逐步調(diào)用到了_objc_init
,這里就是 objc 和 runtime 的初始化入口。
除了 runtime 環(huán)境的初始化外,_objc_init
中綁定了新 image 被加載后的 callback:
dyld_register_image_state_change_handler(
dyld_image_state_bound, 1, &map_images);
dyld_register_image_state_change_handler(
dyld_image_state_dependents_initialized, 0, &load_images);
可見 dyld 擔(dān)當(dāng)了 runtime 和 ImageLoader 中間的協(xié)調(diào)者,當(dāng)新 image 加載進(jìn)來后交由 runtime 去解析這個二進(jìn)制文件的符號表和代碼。繼續(xù)上面的斷點(diǎn)法,斷住神秘的 +load 函數(shù):清楚的看到整個調(diào)用棧和順序:
dyld
開始將程序二進(jìn)制文件初始化
交由ImageLoader
讀取image
,其中包含了我們的類、方法等各種符號
由于runtime
向dyld
綁定了回調(diào),當(dāng) image
加載到內(nèi)存后,dyld
會通知 runtime
進(jìn)行處理
runtime
接手后調(diào)用map_images
做解析和處理,接下來 load_images
中調(diào)用call_load_methods
方法,遍歷所有加載進(jìn)來的Class
,按繼承層級依次調(diào)用Class
的+load
方法和其Category
的 +load
方法
至此,可執(zhí)行文件中和動態(tài)庫所有的符號(Class,Protocol,Selector,IMP,…)
都已經(jīng)按格式成功加載到內(nèi)存中,被runtime
所管理,再這之后,runtime
的那些方法(動態(tài)添加Class、swizzle
等等才能生效)
main函數(shù)
當(dāng)所有的依賴庫庫的 lnitializer 都調(diào)用完后,dyld::main
函數(shù)會返回程序的 main 函數(shù)地址,main 函數(shù)被調(diào)用,從而代碼來到了我們熟悉的程序入口。
結(jié)語
- 整個事件由 dyld 主導(dǎo),完成運(yùn)行環(huán)境的初始化后,配合 ImageLoader 將二進(jìn)制文件按格式加載到內(nèi)存,
- 動態(tài)鏈接依賴庫,并由 runtime 負(fù)責(zé)加載成 objc 定義的結(jié)構(gòu),所有初始化工作結(jié)束后,dyld 調(diào)用真正的 main 函數(shù)。
這里只是簡單了概括了從程序啟動->dyld加載依賴庫->runtime初始化->main 的過程。
參考:
iOS 程序 main 函數(shù)之前發(fā)生了什么
iOS程序啟動->dyld加載->runtime初始化(初識)