我是前言
一個(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)鏈接庫(kù)
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è)試出來(lái):先找到可執(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語(yǔ)言庫(kù) )
- libsystem_blocks ( Block )
- libcommonCrypto ( 加密庫(kù),比如常用的 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é),援引并翻譯《 Mike Ash 這篇 blog 》對(duì) dyld 作用順序的概括:
- 從 kernel 留下的原始調(diào)用棧引導(dǎo)和啟動(dòng)自己
- 將程序依賴的動(dòng)態(tài)鏈接庫(kù)遞歸加載進(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)證出來(lái)缕溉,設(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)庫(kù)過程,由于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í)例來(lái)負(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)來(lái)后交由 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 做解析和處理棺妓,接下來(lái) load_images 中調(diào)用 call_load_methods 方法攘已,遍歷所有加載進(jìn)來(lái)的 Class,按繼承層級(jí)依次調(diào)用 Class 的 +load 方法和其 Category 的 +load 方法
至此怜跑,可執(zhí)行文件中和動(dòng)態(tài)庫(kù)所有的符號(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)鏈接過程中,所有依賴庫(kù)的類是先于自己的類加載的
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)鏈接依賴庫(kù)歉糜,并由 runtime 負(fù)責(zé)加載成 objc 定義的結(jié)構(gòu)乘寒,所有初始化工作結(jié)束后,dyld 調(diào)用真正的 main 函數(shù)匪补。
值得說明的是伞辛,這個(gè)過程遠(yuǎn)比寫出來(lái)的要復(fù)雜,這里只提到了 runtime 這個(gè)分支夯缺,還有像 GCD
蚤氏、XPC
等重頭的系統(tǒng)庫(kù)初始化分支沒有提及(當(dāng)然,有緩存機(jī)制在踊兜,它們也不會(huì)玩命初始化)竿滨,總結(jié)起來(lái)就是 main 函數(shù)執(zhí)行之前,系統(tǒng)做了茫茫多的加載和初始化工作捏境,但都被很好的隱藏了于游,我們無(wú)需關(guān)心纽竣。
孤獨(dú)的 main 函數(shù)
當(dāng)這一切都結(jié)束時(shí)宜雀,dyld 會(huì)清理現(xiàn)場(chǎng),將調(diào)用棧回歸骏掀,只剩下:
孤獨(dú)的 main 函數(shù),看上去是程序的開始柱告,確是一段精彩的終結(jié)
References
https://www.mikeash.com/pyblog/friday-qa-2012-11-09-dyld-dynamic-linking-on-os-x.html
http://newosxbook.com/articles/DYLD.html
http://docstore.mik.ua/orelly/unix3/mac/ch05_02.htm
https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dyld.1.html
聲明
此文引用自iOS程序 main 函數(shù)之前發(fā)生了什么截驮,自從業(yè)以來(lái),從孫源老師那學(xué)到了很多际度,在此表示由衷的感謝葵袭!
其他拓展
dyld:dyld詳解