一些概念
Mach-O是運(yùn)行時產(chǎn)生的可執(zhí)行文件的文件類型
-
Image:
- Executable:程序的主二進(jìn)制文件
- Dylib:動態(tài)庫
- Bundle:是一種特殊的Dylib赦役,是不能進(jìn)行鏈接的,只能在運(yùn)行時用dlopen()函數(shù)打開。
Framework:Dylib+儲存該Dylib需要的資源候址、頭文件的目錄結(jié)構(gòu)
Mach-O Image File被分割成幾個段
-
_TEXT
:頭文件,代碼,只讀常量 -
_DATA
:所有可讀寫內(nèi)容(全局變量、靜態(tài)變量等等) -
_LINKEDIT
:儲存關(guān)于如何加載程序的“元數(shù)據(jù)”
每個段都是page size的倍數(shù)痪蝇,圖中
_TEXT
占有三個page,arm64一個page size是16KB冕房,其它的是4KB
虛擬內(nèi)存把每一個進(jìn)程地址映射到物理內(nèi)存RAM中:
- page錯誤
- 多個進(jìn)程中出現(xiàn)的相同的RAM page
- 文件回溯page:mmap()躏啰、延遲讀取(lazy reading)
- Copy-On-Write(COW)
- 臟page和干凈page
- Permissions:rwx
安全:
ASLR(Address space layout randomization)是一種針對緩沖區(qū)溢出的安全保護(hù)技術(shù),通過對堆毒费、棧丙唧、共享庫映射等線性區(qū)布局的隨機(jī)化,通過增加攻擊者預(yù)測目的地址的難度觅玻,防止攻擊者直接定位攻擊代碼位置,達(dá)到阻止溢出攻擊的目的培漏。據(jù)研究表明ASLR可以有效的降低緩沖區(qū)溢出攻擊的成功率溪厘,如今Linux、FreeBSD牌柄、Windows等主流操作系統(tǒng)都已采用了該技術(shù)畸悬。
ASLR:
- 地址空間隨機(jī)布局
- 鏡像(Images)加載在隨機(jī)的地址
代碼簽名
- 每一個page擁有的內(nèi)容
- page內(nèi)的哈希驗證
exec()到main(),內(nèi)核讓你的App在隨機(jī)的地址開始運(yùn)行
什么是Dyld珊佣?
- Dyld是動態(tài)加載器蹋宦,內(nèi)核加載的輔助程序
- 程序從Dyld開始執(zhí)行
- Dyld運(yùn)行在進(jìn)程中
- Dyld負(fù)責(zé)加載動態(tài)依賴庫
-
Dyld擁有與App相同的權(quán)限
Main()函數(shù)之前的五個階段
Dyld主導(dǎo)的五個階段:
一披粟、Load dylibs:
動態(tài)映射所有的動態(tài)依賴庫。
- 解析動態(tài)依賴庫(dylib)的列表
- 找到所有需要的mach-o(dylib)文件
- 打開并讀取每一個找到的文件
- 驗證這些文件是不是mach-o文件
- 找到它的編碼簽名冷冗,在內(nèi)核里對它進(jìn)行注冊
- 給每一個分段調(diào)用映射
現(xiàn)在守屉,所有App指向的動態(tài)依賴庫都被遞歸加載,App通常需要加載100-400個動態(tài)庫蒿辙,大多數(shù)是OS(操作系統(tǒng))的動態(tài)庫
二拇泛、Rebase
遍歷所有內(nèi)部數(shù)據(jù)指針為他們添加一個滑動值。這些指針的位置都被編碼在
__LINKEDIT
段里思灌。
- 調(diào)整所有鏡像內(nèi)的指針俺叭,添加一個slide偏差值。
Slide = actual_address - preferred_address
- 出于安全考慮泰偿,引入了 ASLR熄守,全稱
Address Space Layout Randomization
。
大概意思就是鏡像(dylib)會加載在隨機(jī)的地址上耗跛,和actual_address會有一個偏差(slide)裕照,dyld需要修正這個偏差,來指向正確的地址课兄。
- Rebase+Bind+ObjC大多數(shù)時間在做修復(fù):
- 代碼簽名意味著命令不能被修改
- 代碼不能被加載到任何地址上牍氛,而且永遠(yuǎn)不能被修改
- 所有的修復(fù)都發(fā)生在
__DATA
數(shù)據(jù)段
三、Bind
遍歷查詢符號表烟阐,設(shè)置指向鏡像外部的指針搬俊。
- 所有在其它動態(tài)庫引用的東西都會符號化
- Dyld需要找到所有符號名
- 會比Rebasing進(jìn)行更多的計算
四、ObjC
通知 runtime 準(zhǔn)備鏡像蜒茄、OC類的注冊唉擂、偏移
ivar
的地址、加載Category
- runtime要維護(hù)一張表檀葛,包含所有類名(
Class Name
)及其映射的類(Class
) - 完成所有OC類的定義注冊
- 運(yùn)行時改變所有
ivar
的偏移量 - 接下來在ObjC階段可以定義分類(
Category
) - 最后讓
Selector
都是唯一的
五玩祟、Initializers
dyld開始調(diào)用C++靜態(tài)構(gòu)造函數(shù),初始化器用來初始化那些抽象DATA
調(diào)用所有類的
+load
方法,順序:父類->子類->Category-
每個Initializers按照從下向上的順序執(zhí)行屿聋。
為什么從下向上空扎?
因為當(dāng)Initializers運(yùn)行時,可能會調(diào)用一些dylib润讥,我們需要確保那些dylib已經(jīng)準(zhǔn)備好被調(diào)用转锈,所以從下開始運(yùn)行Initializers,一直往上到應(yīng)用類楚殿,可以很安全的調(diào)用依賴的內(nèi)容撮慨。
最后Dyld調(diào)用main()函數(shù)
小結(jié):Dyld是一個輔助程序
- 加載所有的依賴庫
- 修復(fù)所有DATA page中的指針
- 運(yùn)行所有的初始化器(Initializers)
- 跳轉(zhuǎn)到主函數(shù)
優(yōu)化啟動時間的實踐部分
- 啟動時間如果在400ms(0.4s)以內(nèi)會讓用戶覺得啟動快
- 啟動時間千萬不要超過20s,否則OS將會認(rèn)為你的APP進(jìn)入死循環(huán),殺死APP
App啟動都做了什么砌溺?
在main函數(shù)之前的五個階段之后影涉,還要調(diào)用:
main()
UIApplicationMain()
: 加載framework的初始化器,加載nibsapplicationWillFinishLaunching
以上8個步驟都算在這400ms內(nèi)规伐。
熱啟動和冷啟動
- 熱啟動:App及其數(shù)據(jù)已經(jīng)在內(nèi)存(磁盤)中
- 冷啟動:App不在內(nèi)核的緩存中
優(yōu)化方案:
一蟹倾、Load Dylib階段
-
合并已有的動態(tài)庫(包括framework),限制動態(tài)庫(包括framework)個數(shù)效果非常明顯
使用靜態(tài)庫代替
-
可以使用延遲加載楷力,也就是使用dlopen()函數(shù)喊式,但是dlopen()會帶來細(xì)微的性能和正確性的問題。
優(yōu)化前的鏈接庫個數(shù)
優(yōu)化前的啟動時間
原有26個framework合并成2個后萧朝,由240ms變?yōu)?1ms岔留。
二、Rebase和Bind階段
-
減少OC元數(shù)據(jù)
-
減少OC類的數(shù)量(不鼓勵使用很多很小的類检柬,只有一兩個方法的那種)
-
減少
selector
的數(shù)量 -
減少Category的數(shù)量
-
減少C++虛擬函數(shù)献联,虛擬函數(shù)創(chuàng)建被稱作 V表格,和OC元數(shù)據(jù)相同
-
避免讓機(jī)器生成過多的代碼何址,機(jī)器生成的指針非常耗內(nèi)存
- 使用偏移量代替指針
-
標(biāo)記為只讀
三里逆、ObjC階段
這個階段的優(yōu)化工作已經(jīng)在Rebase和Bind階段做完了
四、Initializers階段
有兩種Initializers:顯式和隱式
顯式:
+load
方案:使用 +initiailize
代替
- C/C++的
__attribute__((constructor))
方案:使用site initializers
- 使用dispatch_once()
- 使用pthread_once()
- 使用std::once()
隱式:大部分是C++的全局變量帶來的非默認(rèn)初始化器
- 使用site initializers
- 只設(shè)置簡單的值
- 不要在初始化器中調(diào)用dlopen()
-
不要在初始化器中創(chuàng)建線程