一個(gè) iOS App 的main函數(shù)位于 main.m 中雏吭,這是我們熟知的程序入口锁施。但對(duì) objc 了解更多之后發(fā)現(xiàn)揩魂,程序在進(jìn)入我們的 main 函數(shù)前已經(jīng)執(zhí)行了很多代碼剂邮,比如熟知的+ load方法等呜笑。本文將跟隨程序執(zhí)行順序颅痊,刨根問底殖熟,從dyld到runtime,看看 main 函數(shù)之前都發(fā)生了什么斑响。
從dyld開始
動(dòng)態(tài)鏈接庫
iOS 中用到的所有系統(tǒng) framework 都是動(dòng)態(tài)鏈接的菱属,類比成插頭和插排,靜態(tài)鏈接的代碼在編譯后的靜態(tài)鏈接過程就將插頭和插排一個(gè)個(gè)插好舰罚,運(yùn)行時(shí)直接執(zhí)行二進(jìn)制文件纽门;而動(dòng)態(tài)鏈接需要在程序啟動(dòng)時(shí)去完成“插插銷”的過程,所以在我們寫的代碼執(zhí)行前营罢,動(dòng)態(tài)連接器需要完成準(zhǔn)備工作赏陵。
這個(gè)是在 Xcode 中看到的 Link 列表:
這些 framework 將會(huì)在動(dòng)態(tài)鏈接過程中被加載,另外還有隱含 link 的 framework饲漾,可以測(cè)試出來:先找到可執(zhí)行文件蝙搔,我這里叫 TestMain 的工程,模擬器路徑下找到 TestMain.app考传,可執(zhí)行文件默認(rèn)同名吃型,再通過otool命令:
$ otool -L TestMain
-L參數(shù)打印出所有 link 的 framework(去掉了版本信息如下)
TestMain:
/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics
/System/Library/Frameworks/UIKit.framework/UIKit
/System/Library/Frameworks/Foundation.framework/Foundation
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
/usr/lib/libobjc.A.dylib
/usr/lib/libSystem.dylib
除了多了的CoreGraphics(被 UIKit 依賴)外,有兩個(gè)默認(rèn)添加的 lib:libobjc即 objc 和 runtime僚楞,libSystem中包含了很多系統(tǒng)級(jí)別 lib勤晚,列幾個(gè)熟知的:
libdispatch ( GCD )
libsystem_c ( C語言庫 )
libsystem_blocks ( Block )
libcommonCrypto ( 加密庫,比如常用的 md5 函數(shù) )
這些 lib 都是dylib格式(如 windows 中的 dll )泉褐,系統(tǒng)使用動(dòng)態(tài)鏈接有幾點(diǎn)好處:
代碼共用:很多程序都動(dòng)態(tài)鏈接了這些 lib赐写,但它們?cè)趦?nèi)存和磁盤中中只有一份
易于維護(hù):由于被依賴的 lib 是程序執(zhí)行時(shí)才 link 的,所以這些 lib 很容易做更新膜赃,比如libSystem.dylib是libSystem.B.dylib的替身挺邀,哪天想升級(jí)直接換成libSystem.C.dylib然后再替換替身就行了
減少可執(zhí)行文件體積:相比靜態(tài)鏈接,動(dòng)態(tài)鏈接在編譯時(shí)不需要打進(jìn)去,所以可執(zhí)行文件的體積要小很多
dyld
dyld(the dynamic link editor)悠夯,Apple 的動(dòng)態(tài)鏈接器癌淮,系統(tǒng) kernel 做好啟動(dòng)程序的初始準(zhǔn)備后,交給 dyld 負(fù)責(zé)沦补,?dyld 作用順序的概括:
從 kernel 留下的原始調(diào)用棧引導(dǎo)和啟動(dòng)自己
將程序依賴的動(dòng)態(tài)鏈接庫遞歸加載進(jìn)內(nèi)存乳蓄,當(dāng)然這里有緩存機(jī)制
non-lazy 符號(hào)立即 link 到可執(zhí)行文件,lazy 的存表里
Runs static initializers for the executable
找到可執(zhí)行文件的 main 函數(shù)夕膀,準(zhǔn)備參數(shù)并調(diào)用
程序執(zhí)行中負(fù)責(zé)綁定 lazy 符號(hào)虚倒、提供 runtime dynamic loading services、提供調(diào)試器接口
程序main函數(shù) return 后執(zhí)行 static terminator
某些場(chǎng)景下 main 函數(shù)結(jié)束后調(diào) libSystem 的_exit函數(shù)
得益于 dyld 是開源的产舞,github 地址魂奥,我們可以從源碼一探究竟。
一切源于dyldStartup.s這個(gè)文件易猫,其中用匯編實(shí)現(xiàn)了名為__dyld_start的方法耻煤,匯編太生澀,它主要干了兩件事:
調(diào)用dyldbootstrap::start()方法(省去參數(shù))
上個(gè)方法返回了 main 函數(shù)地址准颓,填入?yún)?shù)并調(diào)用 main 函數(shù)
這個(gè)步驟隨手就能驗(yàn)證出來哈蝇,設(shè)置一個(gè)符號(hào)斷點(diǎn)斷在_objc_init:
這個(gè)函數(shù)是runtime的初始化函數(shù),后面會(huì)提到攘已。程序運(yùn)行在很早的時(shí)候斷住炮赦,這時(shí)候看調(diào)用棧:
看到了棧底的dyldbootstrap::start()方法,繼而調(diào)用了dyld::_main()方法样勃,其中完成了剛才說的遞歸加載動(dòng)態(tài)庫過程吠勘,由于libSystem默認(rèn)引入,棧中出現(xiàn)了libSystem_initializer的初始化方法峡眶。
ImageLoader
當(dāng)然這個(gè)image不是圖片的意思剧防,它大概表示一個(gè)二進(jìn)制文件(可執(zhí)行文件或 so 文件),里面是被編譯過的符號(hào)幌陕、代碼等诵姜,所以 ImageLoader 作用是將這些文件加載進(jìn)內(nèi)存,且每一個(gè)文件對(duì)應(yīng)一個(gè)ImageLoader實(shí)例來負(fù)責(zé)加載搏熄。
兩步走:
在程序運(yùn)行時(shí)它先將動(dòng)態(tài)鏈接的 image 遞歸加載 (也就是上面測(cè)試棧中一串的遞歸調(diào)用的時(shí)刻)
再?gòu)目蓤?zhí)行文件 image 遞歸加載所有符號(hào)
當(dāng)然所有這些都發(fā)生在我們真正的main函數(shù)執(zhí)行前。
runtime 與 +load
剛才講到libSystem是若干個(gè)系統(tǒng) lib 的集合暇赤,所以它只是一個(gè)容器 lib 而已心例,而且它也是開源的,里面實(shí)質(zhì)上就一個(gè)文件鞋囊,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 大廚去解析這個(gè)二進(jìn)制文件的符號(hào)表和代碼。繼續(xù)上面的斷點(diǎn)法歉糜,斷住神秘的+load函數(shù):
清楚的看到整個(gè)調(diào)用棧和順序:
dyld 開始將程序二進(jìn)制文件初始化
交由 ImageLoader 讀取 image乘寒,其中包含了我們的類、方法等各種符號(hào)
由于 runtime 向 dyld 綁定了回調(diào)匪补,當(dāng) image 加載到內(nèi)存后伞辛,dyld 會(huì)通知 runtime 進(jìn)行處理
runtime 接手后調(diào)用 map_images 做解析和處理,接下來 load_images 中調(diào)用 call_load_methods 方法夯缺,遍歷所有加載進(jìn)來的 Class蚤氏,按繼承層級(jí)依次調(diào)用 Class 的 +load 方法和其 Category 的 +load 方法
至此,可執(zhí)行文件中和動(dòng)態(tài)庫所有的符號(hào)(Class踊兜,Protocol竿滨,Selector,IMP捏境,…)都已經(jīng)按格式成功加載到內(nèi)存中于游,被 runtime 所管理,再這之后典蝌,runtime 的那些方法(動(dòng)態(tài)添加 Class曙砂、swizzle 等等才能生效)
關(guān)于 +load 方法的幾個(gè) QA
Q: 重載自己 Class 的 +load 方法時(shí)需不需要調(diào)父類?
A: runtime 負(fù)責(zé)按繼承順序遞歸調(diào)用骏掀,所以我們不能調(diào) super
Q: 在自己 Class 的 +load 方法時(shí)能不能替換系統(tǒng) framework(比如 UIKit)中的某個(gè)類的方法實(shí)現(xiàn)
A: 可以鸠澈,因?yàn)閯?dòng)態(tài)鏈接過程中,所有依賴庫的類是先于自己的類加載的
Q: 重載 +load 時(shí)需要手動(dòng)添加 @autoreleasepool 么截驮?
A: 不需要笑陈,在 runtime 調(diào)用 +load 方法前后是加了objc_autoreleasePoolPush()和objc_autoreleasePoolPop()的。
Q: 想讓一個(gè)類的 +load 方法被調(diào)用是否需要在某個(gè)地方 import 這個(gè)文件
A: 不需要葵袭,只要這個(gè)類的符號(hào)被編譯到最后的可執(zhí)行文件中涵妥,+load 方法就會(huì)被調(diào)用(Reveal SDK 就是利用這一點(diǎn),只要引入到工程中就能工作)
簡(jiǎn)單總結(jié)
整個(gè)事件由 dyld 主導(dǎo)坡锡,完成運(yùn)行環(huán)境的初始化后蓬网,配合 ImageLoader 將二進(jìn)制文件按格式加載到內(nèi)存,
動(dòng)態(tài)鏈接依賴庫鹉勒,并由 runtime 負(fù)責(zé)加載成 objc 定義的結(jié)構(gòu)帆锋,所有初始化工作結(jié)束后,dyld 調(diào)用真正的 main 函數(shù)禽额。
值得說明的是锯厢,這個(gè)過程遠(yuǎn)比寫出來的要復(fù)雜皮官,這里只提到了 runtime 這個(gè)分支,還有像GCD实辑、XPC等重頭的系統(tǒng)庫初始化分支沒有提及(當(dāng)然捺氢,有緩存機(jī)制在,它們也不會(huì)玩命初始化)剪撬,總結(jié)起來就是 main 函數(shù)執(zhí)行之前摄乒,系統(tǒng)做了茫茫多的加載和初始化工作,但都被很好的隱藏了婿奔,我們無需關(guān)心缺狠。
孤獨(dú)的 main 函數(shù)
當(dāng)這一切都結(jié)束時(shí),dyld 會(huì)清理現(xiàn)場(chǎng)萍摊,將調(diào)用椉非眩回歸,只剩下:
孤獨(dú)的 main 函數(shù)冰木,看上去是程序的開始穷劈,確是一段精彩的終結(jié)