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

一個(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é)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市踊沸,隨后出現(xiàn)的幾起案子歇终,更是在濱河造成了極大的恐慌,老刑警劉巖逼龟,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件评凝,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡腺律,警方通過查閱死者的電腦和手機(jī)奕短,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匀钧,“玉大人翎碑,你說我怎么就攤上這事≈梗” “怎么了日杈?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)佑刷。 經(jīng)常有香客問我莉擒,道長(zhǎng),這世上最難降的妖魔是什么瘫絮? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任啰劲,我火速辦了婚禮,結(jié)果婚禮上檀何,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好频鉴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布栓辜。 她就那樣靜靜地躺著,像睡著了一般垛孔。 火紅的嫁衣襯著肌膚如雪藕甩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天周荐,我揣著相機(jī)與錄音狭莱,去河邊找鬼。 笑死概作,一個(gè)胖子當(dāng)著我的面吹牛腋妙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播讯榕,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼骤素,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了愚屁?” 一聲冷哼從身側(cè)響起济竹,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎霎槐,沒想到半個(gè)月后送浊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡丘跌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年袭景,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碍岔。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡浴讯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蔼啦,到底是詐尸還是另有隱情榆纽,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布捏肢,位于F島的核電站奈籽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏鸵赫。R本人自食惡果不足惜衣屏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望辩棒。 院中可真熱鬧狼忱,春花似錦膨疏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至窘俺,卻和暖如春饲帅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘤泪。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工灶泵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人对途。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓赦邻,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親掀宋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子深纲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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