iOS:程序main函數(shù)之前發(fā)生了什么

我是前言

一個(gè) iOS App 的 main 函數(shù)位于 main.m 中泡躯,這是我們熟知的程序入口该编。但對(duì) objc 了解更多之后發(fā)現(xiàn)赠摇,程序在進(jìn)入我們的 main 函數(shù)前已經(jīng)執(zhí)行了很多代碼货徙,比如熟知的 + load 方法等芍瑞。本文將跟隨程序執(zhí)行順序隘谣,刨根問底,從 dyldruntime 啄巧,看看 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 列表:

image

這些 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.dyliblibSystem.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 作用順序的概括:

  1. 從 kernel 留下的原始調(diào)用棧引導(dǎo)和啟動(dòng)自己
  2. 將程序依賴的動(dòng)態(tài)鏈接庫(kù)遞歸加載進(jìn)內(nèi)存局待,當(dāng)然這里有緩存機(jī)制
  3. non-lazy 符號(hào)立即 link 到可執(zhí)行文件斑响,lazy 的存表里
  4. Runs static initializers for the executable
  5. 找到可執(zhí)行文件的 main 函數(shù),準(zhǔn)備參數(shù)并調(diào)用
  6. 程序執(zhí)行中負(fù)責(zé)綁定 lazy 符號(hào)钳榨、提供 runtime dynamic loading services舰罚、提供調(diào)試器接口
  7. 程序main函數(shù) return 后執(zhí)行 static terminator
  8. 某些場(chǎng)景下 main 函數(shù)結(jié)束后調(diào) libSystem 的 _exit 函數(shù)

得益于 dyld 是開源的,github 地址薛耻,我們可以從源碼一探究竟营罢。

一切源于dyldStartup.s這個(gè)文件,其中用匯編實(shí)現(xiàn)了名為__dyld_start的方法饼齿,匯編太生澀饲漾,它主要干了兩件事:

  1. 調(diào)用dyldbootstrap::start()方法(省去參數(shù))
  2. 上個(gè)方法返回了 main 函數(shù)地址,填入?yún)?shù)并調(diào)用 main 函數(shù)

這個(gè)步驟隨手就能驗(yàn)證出來(lái)缕溉,設(shè)置一個(gè)符號(hào)斷點(diǎn)斷在_objc_init

image

這個(gè)函數(shù)是runtime的初始化函數(shù)考传,后面會(huì)提到。程序運(yùn)行在很早的時(shí)候斷住证鸥,這時(shí)候看調(diào)用棧:

image

看到了棧底的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é)加載悠夯。

兩步走:

  1. 在程序運(yùn)行時(shí)它先將動(dòng)態(tài)鏈接的 image 遞歸加載 (也就是上面測(cè)試棧中一串的遞歸調(diào)用的時(shí)刻)
  2. 再?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)了 runtimeImageLoader 中間的協(xié)調(diào)者,當(dāng)新 image 加載進(jìn)來(lái)后交由 runtime 大廚去解析這個(gè)二進(jìn)制文件的符號(hào)表和代碼菠剩。繼續(xù)上面的斷點(diǎn)法易猫,斷住神秘的 +load 函數(shù):

image

清楚的看到整個(gè)調(diào)用棧和順序:

  1. dyld 開始將程序二進(jìn)制文件初始化
  2. 交由 ImageLoader 讀取 image,其中包含了我們的類具壮、方法等各種符號(hào)
  3. 由于 runtime 向 dyld 綁定了回調(diào)准颓,當(dāng) image 加載到內(nèi)存后,dyld 會(huì)通知 runtime 進(jìn)行處理
  4. 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)用棧回歸骏掀,只剩下:


image

孤獨(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詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市乖菱,隨后出現(xiàn)的幾起案子坡锡,更是在濱河造成了極大的恐慌蓬网,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹉勒,死亡現(xiàn)場(chǎng)離奇詭異帆锋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)禽额,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門锯厢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人脯倒,你說我怎么就攤上這事实辑。” “怎么了藻丢?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵剪撬,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我郁岩,道長(zhǎng)婿奔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任问慎,我火速辦了婚禮萍摊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘如叼。我一直安慰自己冰木,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布笼恰。 她就那樣靜靜地躺著踊沸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪社证。 梳的紋絲不亂的頭發(fā)上逼龟,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音追葡,去河邊找鬼腺律。 笑死,一個(gè)胖子當(dāng)著我的面吹牛宜肉,可吹牛的內(nèi)容都是我干的匀钧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谬返,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼之斯!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起遣铝,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤佑刷,失蹤者是張志新(化名)和其女友劉穎莉擒,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘫絮,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡啰劲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了檀何。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝇裤。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖频鉴,靈堂內(nèi)的尸體忽然破棺而出栓辜,到底是詐尸還是另有隱情,我是刑警寧澤垛孔,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布藕甩,位于F島的核電站,受9級(jí)特大地震影響周荐,放射性物質(zhì)發(fā)生泄漏狭莱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一概作、第九天 我趴在偏房一處隱蔽的房頂上張望腋妙。 院中可真熱鬧,春花似錦讯榕、人聲如沸骤素。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)济竹。三九已至,卻和暖如春霎槐,著一層夾襖步出監(jiān)牢的瞬間送浊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工丘跌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留袭景,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓碍岔,卻偏偏與公主長(zhǎng)得像浴讯,于是被迫代替她去往敵國(guó)和親朵夏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蔼啦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明先生_X自主閱讀 15,980評(píng)論 3 119
  • 你以為四川是辣是火熱是五光十色是寶馬雕車香滿路 生于斯長(zhǎng)于斯不過只流連了大半個(gè)平原 春熙路涌動(dòng)的人潮 太古里的所所...
    心一呀閱讀 204評(píng)論 0 1
  • 1仰猖、芭芭拉.里斯曼對(duì)這個(gè)觀點(diǎn)感到好奇捏肢,決定進(jìn)一步研究它奈籽。她的發(fā)現(xiàn)挑戰(zhàn)了來(lái)傳統(tǒng)智慧。顯然鸵赫,負(fù)責(zé)照顧孩子或者年邁父...
    鄧潔兒閱讀 145評(píng)論 0 1