所用版本:
- 處理器: Intel Core i9
- MacOS 12.3.1
- Xcode 13.3.1
- dyld-941.4
- objc4-838
雖然蘋果官網(wǎng)發(fā)布的正式版才到dyld-852.2
不過(guò)github上可以下到最新非正式版本, 寫文章時(shí)候最新版本為dyld-941.4
, 估計(jì)以后還會(huì)更新氨鹏。dyld3
到dyld4
我認(rèn)為改動(dòng)還是比較大陨界。
dyld4的針對(duì)于的mach-o
解析器 ( iOS上可執(zhí)行文件格式是Mach-O格式, 下方也有具體解釋) 方面跟dyld3相同,但是引入了 JustInTime 的加載器來(lái)優(yōu)化。
-
dyld3
: 相比dyld2
新增預(yù)構(gòu)建/閉包, 目的是將一些啟動(dòng)數(shù)據(jù)創(chuàng)建為閉包存到本地金吗,下次啟動(dòng)將不再重新解析數(shù)據(jù),而是直接讀取閉包內(nèi)容 -
dyld4
: 采用pre-build + just-in-time
預(yù)構(gòu)建/閉包+實(shí)時(shí)解析的雙解析模式, 將根據(jù)緩存有效與否選擇合適的模式進(jìn)行解析, 同時(shí)也處理了閉包失效時(shí)候需要重建閉包的性能問(wèn)題聂喇。
初看下dyld新舊版本對(duì)比, 看一下dyld
加載流程相較之前改變
我這里先帶入dyld
以及dyld
做了什么 , 先看個(gè)例子
普通的一個(gè)OC項(xiàng)目, ViewController
中加一個(gè)+ (void)load
方法, main
中加一個(gè)函數(shù)SAFuc
運(yùn)行一下, 看下它們走的順序, 結(jié)果如下
會(huì)發(fā)現(xiàn), 先走的load
方法, 再走SAFuc
, 最后走的main
中的Hello world
這塊其實(shí)就會(huì)有疑問(wèn), 不應(yīng)該先走main
的Hello world
么? 所以我們就要看下應(yīng)用程序加載流程
動(dòng)態(tài)庫(kù)/靜態(tài)庫(kù)/編譯過(guò)程
先看下編譯過(guò)程
的流程圖
我們先了解動(dòng)態(tài)庫(kù)
, 靜態(tài)庫(kù)
, 代碼編譯過(guò)程
這幾個(gè)概念, 方便后面探索
靜態(tài)庫(kù) / 動(dòng)態(tài)庫(kù)
通常程序都會(huì)依賴系統(tǒng)一些庫(kù), 庫(kù)是什么呢? 其實(shí)庫(kù)就是一些可執(zhí)行的二進(jìn)制文件
, 能被操作系統(tǒng)加載到內(nèi)存里面中舟奠。庫(kù)分為兩種靜態(tài)庫(kù)
, 動(dòng)態(tài)庫(kù)
靜態(tài)庫(kù)
.a
, .lib
等。鏈接階段
時(shí)靜態(tài)庫(kù)會(huì)被完整地復(fù)制, 一起打包在可執(zhí)行文件中鞍恢,被多次使用就有多份冗余拷貝傻粘。
優(yōu)點(diǎn)
: 編譯完成之后, 鏈接到目標(biāo)程序中, 同時(shí)打包到可執(zhí)行文件里面, 不會(huì)有外部依賴。缺點(diǎn)
: 靜態(tài)庫(kù)會(huì)有兩份, 所以會(huì)導(dǎo)致目標(biāo)程序體積增大
, 對(duì)內(nèi)存, 性能, 速度消耗很大帮掉。并且相同靜態(tài)庫(kù)每個(gè)app中都會(huì)拷貝一份弦悉。
動(dòng)態(tài)庫(kù)
.framework
等。程序編譯時(shí)并不會(huì)鏈接到目標(biāo)程序中蟆炊,目標(biāo)程序只會(huì)存儲(chǔ)指向動(dòng)態(tài)庫(kù)的引用警绩,在程序運(yùn)行時(shí)才被載入。蘋果大部分都是動(dòng)態(tài)庫(kù)
-
優(yōu)點(diǎn)
: 不需要拷貝到目標(biāo)程序,減少App包的體積
多個(gè)App可以使用同一個(gè)動(dòng)態(tài)庫(kù),
共享內(nèi)存, 節(jié)約資源
由于運(yùn)行時(shí)才會(huì)去加載, 那么可以在App不使用時(shí)隨時(shí)對(duì)庫(kù)進(jìn)行替換或更新,
更新靈活
缺點(diǎn)
: 動(dòng)態(tài)載入會(huì)帶來(lái)一部分性能損失, 同時(shí)動(dòng)態(tài)庫(kù)也會(huì)使得程序依賴于外部環(huán)境盅称。一旦動(dòng)態(tài)庫(kù)沒(méi)有或消失, 程序會(huì)出現(xiàn)問(wèn)題肩祥。
代碼編譯過(guò)程
-
源文件
: .h, .m, .cpp, .c等文件 -
預(yù)編譯
: 預(yù)先編譯文件(源文件), 詞法語(yǔ)法分析, 替換宏, 刪除注釋, 展開(kāi)頭文件, 產(chǎn)生.i文件 -
編譯
: 編譯文件, 將.i文件轉(zhuǎn)換為匯編語(yǔ)言, 產(chǎn)生.s文件(匯編文件) -
匯編
: 將匯編文件轉(zhuǎn)換為機(jī)器代碼文件, 產(chǎn)生.o文件 -
鏈接
: 把之前所有操作的文件鏈接到程序里面來(lái), 對(duì).o文件中引用其他庫(kù)的地方進(jìn)行引用, 生成最后的可執(zhí)行文件
后室。動(dòng)態(tài)庫(kù)與靜態(tài)庫(kù)區(qū)別其實(shí)就是鏈接的區(qū)別。
可執(zhí)行文件位置: 通常編譯后的 程序.cpp顯示包內(nèi)容, 可找到可執(zhí)行文件
(黑黑的一個(gè)文件)
其實(shí)可執(zhí)行文件就是能夠運(yùn)行起來(lái)的文件, 我們也可以把它拖到終端中回車, 可發(fā)現(xiàn)也能打印出信息混狠。(ios項(xiàng)目需要真機(jī)運(yùn)行, 直接拖入終端回車會(huì)報(bào)錯(cuò))
當(dāng)然我們?nèi)绻胍樵兿到y(tǒng)動(dòng)態(tài)庫(kù)可執(zhí)行文件, 以CoreFoundation
為例
-
斷點(diǎn)
→image list
→CoreFoundation
按路徑搜索 可以找到CoreFoundation.frame
-
CoreFoundation.frame
右鍵選擇顯示包內(nèi)容
即可看到CoreFoundation
的可執(zhí)行文件
dyld
dyld
(the dynamic link editor)是蘋果的動(dòng)態(tài)鏈接器
岸霹,是蘋果操作系統(tǒng)的重要組成部分,在app被編譯打包成可執(zhí)行文件格式的Mach-O
文件后将饺,交由dyld
負(fù)責(zé)連接
動(dòng)靜態(tài)庫(kù)贡避,加載程序
- 這里的
image
不是圖片是鏡像文件
, 庫(kù)加載進(jìn)去就是映射, 映射一份到內(nèi)存, 而這個(gè)東西就image
。映射可以理解成, 例如 動(dòng)態(tài)庫(kù)都存在沙盒路徑磁盤里面, 當(dāng)我們用到相應(yīng)動(dòng)態(tài)庫(kù)時(shí)候, copy一份(找了一個(gè)替身)加載到我們用到程序的內(nèi)存里面予弧。
探索dyld之前我們要先了解入口, 在load方法處加一個(gè)斷點(diǎn)刮吧,bt
查看下, 當(dāng)然也可以通過(guò)左側(cè)的堆棧信息查看。
舊版本
新版本
棧結(jié)構(gòu), 先進(jìn)后出, 所以要從后往前看掖蛤。
- 舊版本
dyld
:_dyld_start
開(kāi)始, 接下來(lái)走dyldbootstrap
, 源碼入口需要在dyld_start
開(kāi)始杀捻。 - 新版本
dyld
:start
開(kāi)始接下來(lái)走dyld4
中prepare
方法, 源碼入口需要在start
中開(kāi)始探索。
當(dāng)然我們也可以走下匯編看下
dyld`在哪里,
舊版本
可發(fā)現(xiàn)在libdyld.dylib
這里面
既然在
libdyld.dylib
里面, 那我們可以去蘋果官方 Source Browser 可下到dyld
源碼, 如下圖
新版本
而新版本......不得不說(shuō)官方很嚴(yán)謹(jǐn)蚓庭。(后面拿真機(jī)做例子)
舊版本
在dyld源碼
之后全局搜索dyld_start
(找入口), 入口是匯編寫的, 看arm環(huán)境
的就行致讥。 能找到接下去走dyldbootstrap
, 這也跟之前bt
打印內(nèi)容一致
dyldStartup
→__dyld_start
(入口函數(shù))查找時(shí)發(fā)現(xiàn),是由dyld
是匯編
實(shí)現(xiàn)(.s匯編文件)器赞,通過(guò)注釋發(fā)現(xiàn), 下面會(huì)調(diào)用call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
垢袱,是一個(gè)C++方法, 那么我們根據(jù)名字dyldbootstrap
, 去尋找他的start
方法。
新版本
新版本我們直接搜索dyldbootstrap
, 肯定是無(wú)了
我們先搜索start
, 在同樣的dyldStartup
有
其實(shí)注釋已經(jīng)告訴我們, 此匯編代碼對(duì)齊堆棧并跳入C代碼:dyld:: start(const KernelArgs* kernArgs)
那么搜索 start(const KernelArgs* kernArgs)
方法看一下, 有
這里的start
方法是dyld的入口點(diǎn)港柜。那么我們接著這里進(jìn)行探索请契。 往下看有一個(gè)"prepare"準(zhǔn)備 方法MainFunc appMain = prepare(state, dyldMA);
,
可看到這個(gè)方法是處理相關(guān)依賴綁定的方法, 那么進(jìn)入看下其源碼, 看看到底準(zhǔn)備些什么內(nèi)容
[ 配置環(huán)境/平臺(tái)/路徑/版本等信息 ]
看下gProcessInfo
有struct dyld_all_image_infos* gProcessInfo = &dyld_all_image_infos;
是一個(gè)存儲(chǔ)dyld所有鏡像信息的一個(gè)結(jié)構(gòu)體
可看出dyld_all_image_infos
包含信息比較多, mach_header
, dyld_uuid_info
, dyldVersion
等等。
其中mach_header
是Mach-O
的頭部夏醉,而dyld
加載的文件就是Mach-O
類型的姚糊,即Mach-O
類型是可執(zhí)行文件類型
,由四部分組成:Mach-O頭部
授舟、Load Command
救恨、section
、Other Data
释树,可以通過(guò)MachOView
可查看可執(zhí)行文件信息
回到prepare
方法, 接著往下看
[進(jìn)行pre-build, 創(chuàng)建mainLoader]
接下來(lái)會(huì)創(chuàng)建一個(gè)mainLoader
主裝載器, 如果熟悉dyld3
的小伙伴知道, 舊版本是創(chuàng)建一個(gè)ImageLoader
鏡像裝載器
mainLoader
主裝載器, 可以理解成一個(gè)容器, 這里面陸續(xù)添加 可執(zhí)行文件
, 動(dòng)態(tài)庫(kù)
等等, 都裝載完成之后經(jīng)由后續(xù)一些處理, 就是我們打開(kāi)的App肠槽。
[ 創(chuàng)建just-in-time ]
這是dyld4
一個(gè)新特性, dyld4
在保留了dyld3
的 mach-o 解析器
基礎(chǔ)上,同時(shí)也引入了 just-in-time
的加載器來(lái)優(yōu)化, 這里稍微細(xì)說(shuō)一下奢啥。
首先dyld3
出于對(duì)啟動(dòng)速度的優(yōu)化的目的, 增加了預(yù)構(gòu)建(閉包)
秸仙。App第一次啟動(dòng)或者App發(fā)生變化時(shí)會(huì)將部分啟動(dòng)數(shù)據(jù)創(chuàng)建為閉包
存到本地,那么App下次啟動(dòng)將不再重新解析數(shù)據(jù)桩盲,而是直接讀取閉包內(nèi)容寂纪。當(dāng)然前提是應(yīng)用程序和系統(tǒng)應(yīng)很少發(fā)生變化,但如果這兩者經(jīng)常變化等, 就會(huì)導(dǎo)閉包丟失或失效。所以dyld4
采用了 pre-build + just-in-time
的雙解析模式捞蛋,預(yù)構(gòu)建 pre-build
對(duì)應(yīng)的就是 dyld3
中的閉包孝冒,just-in-time
可以理解為實(shí)時(shí)解析
。當(dāng)然just-in-time
也是可以利用 pre-build
的緩存的拟杉,所以性能可控庄涡。有了just-in-time
, 目前應(yīng)用首次啟動(dòng)、系統(tǒng)版本更新搬设、普通啟動(dòng)穴店,dyld4
則可以根據(jù)緩存是否有效選擇合適的模式進(jìn)行解析。
[ 裝載內(nèi)容 ]
往下看可看到, mainLoader
進(jìn)行裝載, 裝載可執(zhí)行文件, 動(dòng)態(tài)庫(kù)等等
記錄插入信息, 遍歷所有dylibs, 一些記錄檢查操作繼續(xù)往下走拿穴。
[插入緩存]
這里是對(duì)dyld
緩存的一個(gè)處理, 其中state
是prepare
傳入進(jìn)來(lái)的參數(shù), 其定義APIs& state = APIs::bootstrap(config, sLocks);
是APIs方法里面的bootstrap
引導(dǎo)程序方法泣洞。
接下來(lái)是一些其他通知和寫入操作, 簡(jiǎn)單看一下, 之后是下一個(gè)重點(diǎn)內(nèi)容
runAllInitializersForMain
[運(yùn)行初始化方法]
前面稍微提過(guò)state
定義APIs& state = APIs::bootstrap(config, sLocks);
, 源自DyldAPIs
, 那么我們進(jìn)入看一下
notifyObjCInit 函數(shù)
在執(zhí)行完初始化之后會(huì)執(zhí)行notifyObjCInit
, 告訴objc 去運(yùn)行所有 +load
方法, 而此時(shí)系統(tǒng)main
還沒(méi)有執(zhí)行, 這也就是為什么+ load
方法執(zhí)行在main
前面的原因。我們看一下notifyObjCInit
內(nèi)部
其中 _notifyObjCInit
我們看一下默色。首先可以看到_notifyObjCInit
定義是_dyld_objc_notify_init _notifyObjCInit = nullptr;
因?yàn)榕袛嗍且?code>_notifyObjCInit 非null才繼續(xù)后面, 所以我們要搜索下_notifyObjCInit
什么地方賦值
_notifyObjCInit
是 setObjCNotifiers
方法中的第二參數(shù)_dyld_objc_notify_init init
繼續(xù)找
setObjCNotifiers
有_dyld_objc_notify_register
球凰。
這個(gè)方法其實(shí)在objc4
源碼_objc_init
方法中見(jiàn)過(guò)
我們?cè)?code>objc_init內(nèi)部調(diào)用了dyld_objc_notify_register
方法, 并為其傳入?yún)?shù)load_images
(第二個(gè)參數(shù) init)骤公。
由load_images
→ call_load_methods
→ call_class_loads
內(nèi)部也可以看出會(huì) 循環(huán)調(diào)用所有+load
方法平项,直到不再有荠察。
[link動(dòng)態(tài)庫(kù)和主程序]
回到runAllInitializersForMain
繼續(xù)看, runInitializersBottomUpPlusUpwardLinks
循環(huán)link動(dòng)態(tài)庫(kù), 再link可執(zhí)行文件
[加載主程序入口]
runAllInitializersForMain
準(zhǔn)備工作完成之后, 尋找App中main函數(shù)
, App正常運(yùn)行
綜上也驗(yàn)證了dyld 打印信息