在講dyld流程之前昂儒,我先提一個(gè)問題沟使,就是在我們程序運(yùn)行的時(shí)候,在main
函數(shù)之前渊跋,會(huì)先走ViewController的load
方法腊嗡, 再走C++
的方法着倾,這是為什么?
帶著這個(gè)問題叽唱,我們開始今天的探索之旅屈呕。
首先我們都知道在程序跑起來之前,依賴于很多庫棺亭,比如說動(dòng)態(tài)庫和靜態(tài)庫虎眨,我們稱為鏡像文件images
,這些庫和文件在加載的時(shí)候都需要用到dyld
程序進(jìn)行鏈接镶摘,dyld
是蘋果的動(dòng)態(tài)鏈接器嗽桩,在程序加載前一個(gè)非常重要部分。鏈接完了之后就會(huì)生成一個(gè)可執(zhí)行文件exec
凄敢。 流程如下:
那么明白這一點(diǎn)之后碌冶,接下來我們分析整個(gè)的流程必然就從dyld
入手,dyld
是開源的涝缝,先從蘋果開發(fā)者官網(wǎng)下載一份dyld
扑庞,我這里下載的是750.6版本。
把dyld
打開拒逮,打開之后發(fā)現(xiàn)里面東西很多罐氨,不知道從哪入手,不過看過我之前文章的人滩援,我相信應(yīng)該知道堆棧這個(gè)東西栅隐,就是bt,我們?cè)?code>load里面打個(gè)斷點(diǎn)bt一下
bt之后我們可以看到最后一行的dyld
_dyld_start玩徊,所以
dyld入口就在
_dyld_start`里面租悄,接下來我們?nèi)炙阉髡业较嚓P(guān)源碼
找到相應(yīng)的入口之后,可以看到是通過一些匯編寫的恩袱,看不懂沒關(guān)系泣棋,旁邊有注釋,看注釋看到了這一句# call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
憎蛤,這就好辦了外傅,這是一個(gè)C++的開始函數(shù),我們跟過去
跟過來之后俩檬,我們看到這里有一個(gè)main
萎胰,我們可以從堆棧里面看出這是第二個(gè)流程。我們點(diǎn)到main
里面去棚辽。
發(fā)現(xiàn)里面有大幾百行代碼技竟,從頭看到尾肯定不行, 那么看一下最后它最后的返回值是result
屈藐,然后找到賦值的地方榔组,發(fā)現(xiàn)主要是sMainExecutable
這個(gè)主程序賦的值熙尉,那么接下來我們又開始找sMainExecutable
這個(gè)玩意,最終發(fā)現(xiàn)它在6577行sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
在這個(gè)地方進(jìn)行了初始化搓扯,那么這個(gè)主程序是怎么進(jìn)行初始化的呢检痰,我們繼續(xù)點(diǎn)到instantiateFromLoadedImage
里面去看一看
進(jìn)入instantiateFromLoadedImage
源碼后,發(fā)現(xiàn)創(chuàng)建了一個(gè)ImageLoader
實(shí)例對(duì)象锨推,通過instantiateMainExecutable
方法創(chuàng)建
再進(jìn)到instantiateMainExecutable
里面铅歼,其作用是為主可執(zhí)行文件創(chuàng)建映像,返回一個(gè)ImageLoader類型的image
對(duì)象换可,即主程序
椎椰。其中sniffLoadCommands
函數(shù)時(shí)獲取Mach-O
類型文件的Load Command
的相關(guān)信息,并對(duì)其進(jìn)行各種校驗(yàn)
說到這里沾鳄,我們的dyld
究竟做了些什么事情慨飘, 主要分為以下幾步:
1、環(huán)境變量的配置(根據(jù)環(huán)境變量設(shè)置相應(yīng)的值以及獲取當(dāng)前運(yùn)行架構(gòu))
2译荞、檢查共享緩存 (檢查是否開啟瓤的,以及共享緩存是否映射到共享區(qū)域,例如UIKit吞歼、CoreFoundation等)
3堤瘤、主程序的初始化 (調(diào)用instantiateFromLoadedImage函數(shù)實(shí)例化了一個(gè)ImageLoader對(duì)象)
4、 加入動(dòng)態(tài)庫(遍歷DYLD_INSERT_LIBRARIES環(huán)境變量浆熔,調(diào)用loadInsertedDylib)
5、link 鏈接主程序
6桥帆、link 鏈接動(dòng)態(tài)庫
7医增、main()(把鏈接起來的所有東西運(yùn)行起來,并發(fā)送通知)
那么我們是怎么進(jìn)行initializers
初始化的呢老虫,我們點(diǎn)進(jìn)去看一下
來到initializeMainExecutable里面之后,我們看到主要是循環(huán)遍歷叶骨,執(zhí)行runInitializers
方法。 我們?cè)偃炙阉?code>runInitializers(cons,找到源碼祈匙,其核心代碼是processInitializers
函數(shù)的調(diào)用
繼續(xù)全局搜索processInitializers
忽刽,來到processInitializers
函數(shù)源碼實(shí)現(xiàn)
這里重點(diǎn)在594行,對(duì)我們的鏡像列表調(diào)用recursiveInitialization
進(jìn)行遞歸實(shí)例化夺欲,我們繼續(xù)全局搜索recursiveInitialization
一探到底
這個(gè)地方有兩個(gè)重點(diǎn)跪帝,第一個(gè)重點(diǎn)是1595行和1603號(hào)都調(diào)用了notifySingle
,并傳入了dyld_image
些阅。 第二個(gè)重點(diǎn)是1598行this->doInitialization(context);
伞剑,我們先來看看notifySingle
做了什么操作
這里的重點(diǎn)是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
這一句,然后再全局搜索sNotifyObjCInit
市埋,發(fā)現(xiàn)沒有找到實(shí)現(xiàn)黎泣,只有賦值的操作
接著我們搜索registerObjCNotifiers
在哪個(gè)地方調(diào)用了恕刘,結(jié)果發(fā)現(xiàn)在_dyld_objc_notify_register
調(diào)用了
_dyld_objc_notify_register
這個(gè)函數(shù)不知道大家熟不熟悉,反正它離我們很近抒倚,結(jié)果在我們libobjc
源碼中一搜索褐着,就在我們的_objc_init
里面
看到這里,sNotifyObjCInit
的賦值的就是來自objc
中的load_images
托呕,那么_objc_init
是什么時(shí)候調(diào)用的呢含蓉,接下來我們回到上面說的二個(gè)重點(diǎn)的第二個(gè)重點(diǎn)this->doInitialization(context);
我們進(jìn)入到doInitialization
源碼實(shí)現(xiàn)
這里有兩句重點(diǎn),我們先來看第一句doImageInit
進(jìn)入到doImageInit
之后镣陕,其核心主要是for循環(huán)加載方法的調(diào)用谴餐,這里需要注意的一點(diǎn)是,libSystem
庫的要求很高呆抑,需要先初始化運(yùn)行岂嗓,這里也標(biāo)了注釋libSystem initializer must run first
再來看看doModInitFunctions
源碼,這個(gè)方法中加載了所有Cxx文件
說了這么多鹊碍,現(xiàn)在在load方法打個(gè)斷點(diǎn)來看看堆棧和整個(gè)初始化過程
雖然把整個(gè)堆棧過程打印出來了厌殉,但是沒有看到_objc_init
的調(diào)用,我們?cè)偌觽€(gè)符號(hào)斷點(diǎn)看一下
來到_objc_init
之后侈咕,前面的流程都一樣公罕,來看一下libSystem_initializer
。在libsystem
工程中查找libSystem_initializer
耀销,查看源碼實(shí)現(xiàn)
來到這個(gè)源碼之后楼眷,我們看到走了libdispatch_init
函數(shù),在我們初始化過程里面
這個(gè)函數(shù)在libSystem_initializer
前面熊尉,源碼是在libdispatch.dylib
開源庫中的罐柳,接下來我們找到libdispatch
搜索libdispatch_init
,找到實(shí)現(xiàn)的源碼如圖:
在libdispatch_init
源碼里我們看到了_os_object_init
狰住,也在我們初始化過程里面张吉,我們繼續(xù)跟過去
跟過來之后,發(fā)現(xiàn)第一句就調(diào)用了_objc_init
催植,_objc_init
里面又調(diào)用了_dyld_objc_notify_register
進(jìn)行注冊(cè)肮蛹,傳了第二個(gè)參數(shù)load_images
。 注冊(cè)了之后回到dyld里面的notifySingle
创南, 然后會(huì)跳到sNotifyObjCInit = 參數(shù)2
調(diào)用sNotifyObjcInit()
伦忠,形成了一個(gè)閉環(huán)
看到這里,總的流程總算是看完了稿辙,我這篇文章截圖代碼沒有注釋缓苛,所以不一定要搞懂代碼的意思,只需了解dyld
大致流程即可,畢竟這些代碼沒幾個(gè)人能玩的通轉(zhuǎn)未桥。 整個(gè)流程如圖:
明白了dyld
的整體流程之后笔刹,我們?cè)賮砜次恼麻_始前提到的一個(gè)問題就很好分析了
首先在程序加載的時(shí)候來到objc_init
調(diào)用_dyld_objc_notify_register
然后執(zhí)行load_images
,load_images
里面有call_load_methods
-> call_class_loads
-> (*load_method)(cls, @selector(load))
調(diào)用所有類的load
調(diào)用完load
之后會(huì)來到doInitialization
里面的doModInitFunctions
冬耿,在doModInitFunctions
會(huì)調(diào)用所有Cxx函數(shù)
可以打斷點(diǎn)bt
驗(yàn)證一下
走完Cxx函數(shù)之后我們接著往下走
走完之后會(huì)回到_dyld_start
舌菜,此時(shí)register read
讀一下寄存器, rax
已經(jīng)是main at main.m:15
亦镶,然后循環(huán)完之后會(huì)jmpq
跳到main
函數(shù)里面日月。
這就是為什么調(diào)用順序是load -> Cxx -> main
最后注意一點(diǎn),main
是寫定的函數(shù)缤骨,寫入內(nèi)存爱咬,讀取到dyld
,所以main
函數(shù)的名稱是不能改的绊起,改了就會(huì)報(bào)錯(cuò)