App 運行理論設計模式
main() 執行前發生的事緩存
Mach-O 格式安全
虛擬內存基礎數據結構
Mach-O 二進制的加載多線程
理論速成架構
Mach-O 術語app
Mach-O 是針對不一樣運行時可執行文件的文件類型。dom
文件類型:ide
Executable: 應用的主要二進制函數
Dylib: 動態連接庫(又稱 DSO 或 DLL)
Bundle: 不能被連接的 Dylib,只能在運行時使用 dlopen() 加載,可當作 macOS 的插件。
Image: executable,dylib 或 bundle
Framework: 包含 Dylib 以及資源文件和頭文件的文件夾
Mach-O 鏡像文件
Mach-O 被劃分紅一些 segement,每一個 segement 又被劃分紅一些 section。
segment 的名字都是大寫的,且空間大小爲頁的整數。頁的大小跟硬件有關,在 arm64 架構一頁是 16KB,其他爲 4KB。
section 雖然沒有整數倍頁大小的限制,可是 section 之間不會有重疊。
幾乎全部 Mach-O 都包含這三個段(segment): __TEXT,__DATA 和 __LINKEDIT:
__TEXT 包含 Mach header,被執行的代碼和只讀常量(如C 字符串)。只讀可執行(r-x)。
__DATA 包含全局變量,靜態變量等。可讀寫(rw-)。
__LINKEDIT 包含了加載程序的『元數據』,好比函數的名稱和地址。只讀(r–)。
Mach-O Universal 文件
FAT 二進制文件,將多種架構的 Mach-O 文件合併而成。它經過 Fat Header 來記錄不一樣架構在文件中的偏移量,Fat Header 佔一頁的空間。
按分頁來存儲這些 segement 和 header 會浪費空間,但這有利於虛擬內存的實現。
虛擬內存
虛擬內存就是一層間接尋址(indirection)。軟件工程中有句格言就是任何問題都能經過添加一個間接層來解決。虛擬內存解決的是管理全部進程使用物理 RAM 的問題。經過添加間接層來讓每一個進程使用邏輯地址空間,它能夠映射到 RAM 上的某個物理頁上。這種映射不是一對一的,邏輯地址可能映射不到 RAM 上,也可能有多個邏輯地址映射到同一個物理 RAM 上。針對第一種狀況,當進程要存儲邏輯地址內容時會觸發 page fault;第二種狀況就是多進程共享內存。
對於文件能夠不用一次性讀入整個文件,可使用分頁映射(mmap())的方式讀取。也就是把文件某個片斷映射到進程邏輯內存的某個頁上。當某個想要讀取的頁沒有在內存中,就會觸發 page fault,內核只會讀入那一頁,實現文件的懶加載。
也就是說 Mach-O 文件中的 __TEXT 段能夠映射到多個進程,並能夠懶加載,且進程之間共享內存。__DATA 段是可讀寫的。這裏使用到了 Copy-On-Write 技術,簡稱 COW。也就是多個進程共享一頁內存空間時,一旦有進程要作寫操做,它會先將這頁內存內容複製一份出來,而後從新映射邏輯地址到新的 RAM 頁上。也就是這個進程本身擁有了那頁內存的拷貝。這就涉及到了 clean/dirty page 的概念。dirty page 含有進程本身的信息,而 clean page 能夠被內核從新生成(從新讀磁盤)。因此 dirty page 的代價大於 clean page。
Mach-O 鏡像 加載
因此在多個進程加載 Mach-O 鏡像時 __TEXT 和 __LINKEDIT 由於只讀,都是能夠共享內存的。而 __DATA 由於可讀寫,就會產生 dirty page。當 dyld 執行結束後,__LINKEDIT 就沒用了,對應的內存頁會被回收。
安全
ASLR(Address Space Layout Randomization):地址空間佈局隨機化,鏡像會在隨機的地址上加載。這實際上是一二十年前的舊技術了。
代碼簽名:可能咱們認爲 Xcode 會把整個文件都作加密 hash 並用作數字簽名。其實爲了在運行時驗證 Mach-O 文件的簽名,並非每次重複讀入整個文件,而是把每頁內容都生成一個單獨的加密散列值,並存儲在 __LINKEDIT 中。這使得文件每頁的內容都能及時被校驗確並保不被篡改。
從 exec() 到 main()
exec() 是一個系統調用。系統內核把應用映射到新的地址空間,且每次起始位置都是隨機的(由於使用 ASLR)。並將起始位置到0x000000 這段範圍的進程權限都標記爲不可讀寫不可執行。若是是 32 位進程,這個範圍至少是 4KB;對於 64 位進程則至少是 4GB。NULL 指針引用和指針截斷偏差都是會被它捕獲。
dyld 加載 dylib 文件
Unix 的前二十年很安逸,由於那時尚未發明動態連接庫。有了動態連接庫後,一個用於加載連接庫的幫助程序被建立。在蘋果的平臺裏是 dyld,其餘 Unix 系統也有 ld.so。 當內核完成映射進程的工做後會將名字爲 dyld 的Mach-O 文件映射到進程中的隨機地址,它將 PC 寄存器設爲 dyld 的地址並運行。dyld 在應用進程中運行的工做是加載應用依賴的全部動態連接庫,準備好運行所需的一切,它擁有的權限跟應用同樣。
下面的步驟構成了 dyld 的時間線:
Load dylibs -> Rebase -> Bind -> ObjC -> Initializers
加載 Dylib
從主執行文件的 header 獲取到須要加載的所依賴動態庫列表,而 header 早就被內核映射過。而後它須要找到每一個 dylib,而後打開文件讀取文件起始位置,確保它是 Mach-O 文件。接着會找到代碼簽名並將其註冊到內核。而後在 dylib 文件的每一個 segment 上調用mmap()。應用所依賴的 dylib 文件可能會再依賴其餘 dylib,因此 dyld 所須要加載的是動態庫列表一個遞歸依賴的集合。通常應用會加載 100 到 400 個 dylib 文件,但大部分都是系統 dylib,它們會被預先計算和緩存起來,加載速度很快。
Fix-ups
在加載全部的動態連接庫以後,它們只是處在相互獨立的狀態,須要將它們綁定起來,這就是 Fix-ups。代碼簽名使得咱們不能修改指令,那樣就不能讓一個 dylib 的調用另外一個 dylib。這時須要加不少間接層。
現代 code-gen 被叫作動態 PIC(Position Independent Code),意味着代碼能夠被加載到間接的地址上。當調用發生時,code-gen 實際上會在 __DATA 段中建立一個指向被調用者的指針,而後加載指針並跳轉過去。
因此 dyld 作的事情就是修正(fix-up)指針和數據。Fix-up 有兩種類型,rebasing 和 binding。
Rebasing 和 Binding
Rebasing:在鏡像內部調整指針的指向
Binding:將指針指向鏡像外部的內容
能夠經過命令行查看 rebase 和 bind 等信息:
1
|
xcrun dyldinfo -rebase -bind -lazy_bind myapp.app/myapp
|
經過這個命令能夠查看全部的 Fix-up。rebase,bind,weak_bind,lazy_bind 都存儲在 __LINKEDIT 段中,並可經過LC_DYLD_INFO_ONLY 查看各類信息的偏移量和大小。
建議用 MachOView 查看更加方便直觀。
從 dyld 源碼層面簡要介紹下 Rebasing 和 Binding 的流程。
ImageLoader 是一個用於加載可執行文件的基類,它負責連接鏡像,但不關心具體文件格式,由於這些都交給子類去實現。每一個可執行文件都會對應一個 ImageLoader 實例。ImageLoaderMachO 是用於加載 Mach-O 格式文件的 ImageLoader 子類,而ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 都繼承於 ImageLoaderMachO,分別用於加載那些 __LINKEDIT 段爲傳統格式和壓縮格式的 Mach-O 文件。
由於 dylib 之間有依賴關係,因此 ImageLoader 中的好多操做都是沿着依賴鏈遞歸操做的,Rebasing 和 Binding 也不例外,分別對應着 recursiveRebase() 和 recursiveBind() 這兩個方法。由於是遞歸,因此會自底向上地分別調用 doRebase() 和 doBind()方法,這樣被依賴的 dylib 老是先於依賴它的 dylib 執行 Rebasing 和 Binding。傳入 doRebase() 和 doBind() 的參數包含一個LinkContext 上下文,存儲了可執行文件的一堆狀態和相關的函數。
在 Rebasing 和 Binding 前會判斷是否已經 Prebinding。若是已經進行過預綁定(Prebinding),那就不須要 Rebasing 和 Binding 這些 Fix-up 流程了,由於已經在預先綁定的地址加載好了。
ImageLoaderMachO 實例不使用預綁定會有五個緣由:
Mach-O Header 中 MH_PREBOUND 標誌位爲 0
鏡像加載地址有偏移(這個後面會講到)
依賴的庫有變化
鏡像使用 flat-namespace,預綁定的一部分會被忽略
LinkContext 的環境變量禁止了預綁定
ImageLoaderMachO 中 doRebase() 作的事情大體以下:
若是使用預綁定,fgImagesWithUsedPrebinding 計數加一,並 return;不然進入第二步
若是 MH_PREBOUND 標誌位爲 1(也就是能夠預綁定但沒使用),且鏡像在共享內存中,重置上下文中全部的 lazy pointer。(若是鏡像在共享內存中,稍後會在 Binding 過程當中綁定,因此無需重置)
若是鏡像加載地址偏移量爲0,則無需 Rebasing,直接 return;不然進入第四步
調用 rebase() 方法,這纔是真正作 Rebasing 工做的方法。若是開啓 TEXT_RELOC_SUPPORT 宏,會容許 rebase() 方法對__TEXT 段作寫操做來對其進行 Fix-up。因此其實 __TEXT 只讀屬性並非絕對的。
ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 分別實現了本身的 doRebase() 方法。實現邏輯大同小異,一樣會判斷是否使用預綁定,並在真正的 Binding 工做時判斷 TEXT_RELOC_SUPPORT 宏來決定是否對 __TEXT 段作寫操做。最後都會調用setupLazyPointerHandler 在鏡像中設置 dyld 的 entry point,放在最後調用是爲了讓主可執行文件設置好 __dyld 或__program_vars。
Rebasing
在過去,會把 dylib 加載到指定地址,全部指針和數據對於代碼來講都是對的,dyld 就無需作任何 fix-up 了。現在用了 ASLR 後悔將 dylib 加載到新的隨機地址(actual_address),這個隨機的地址跟代碼和數據指向的舊地址(preferred_address)會有誤差,dyld 須要修正這個誤差(slide),作法就是將 dylib 內部的指針地址都加上這個偏移量,偏移量的計算方法以下:
Slide = actual_address - preferred_address
而後就是重複不斷地對 __DATA 段中須要 rebase 的指針加上這個偏移量。這就又涉及到 page fault 和 COW。這可能會產生 I/O 瓶頸,但由於 rebase 的順序是按地址排列的,因此從內核的角度來看這是個有次序的任務,它會預先讀入數據,減小 I/O 消耗。
Binding
Binding 是處理那些指向 dylib 外部的指針,它們實際上被符號(symbol)名稱綁定,也就是個字符串。以前提到 __LINKEDIT 段中也存儲了須要 bind 的指針,以及指針須要指向的符號。dyld 須要找到 symbol 對應的實現,這須要不少計算,去符號表裏查找。找到後會將內容存儲到 __DATA 段中的那個指針中。Binding 看起來計算量比 Rebasing 更大,但其實須要的 I/O 操做不多,由於以前 Rebasing 已經替 Binding 作過了。
ObjC Runtime
Objective-C 中有不少數據結構都是靠 Rebasing 和 Binding 來修正(fix-up)的,好比 Class 中指向超類的指針和指向方法的指針。
ObjC 是個動態語言,能夠用類的名字來實例化一個類的對象。這意味着 ObjC Runtime 須要維護一張映射類名與類的全局表。當加載一個 dylib 時,其定義的全部的類都須要被註冊到這個全局表中。
C++ 中有個問題叫作易碎的基類(fragile base class)。ObjC 就沒有這個問題,由於會在加載時經過 fix-up 動態類中改變實例變量的偏移量。
在 ObjC 中能夠經過定義類別(Category)的方式改變一個類的方法。有時你想要添加方法的類在另外一個 dylib 中,而不在你的鏡像中(也就是對系統或別人的類動刀),這時也須要作些 fix-up。
ObjC 中的 selector 必須是惟一的。
Initializers
C++ 會爲靜態建立的對象生成初始化器。而在 ObjC 中有個叫 +load 的方法,然而它被廢棄了,如今建議使用 +initialize。對比詳見:http://stackoverflow.com/questions/13326435/nsobject-load-and-initialize-what-do-they-do
如今有了主執行文件,一堆 dylib,其依賴關係構成了一張巨大的有向圖,那麼執行初始化器的順序是什麼?自頂向上!按照依賴關係,先加載葉子節點,而後逐步向上加載中間節點,直至最後加載根節點。這種加載順序確保了安全性,加載某個 dylib 前,其所依賴的其他 dylib 文件確定已經被預先加載。
最後 dyld 會調用 main() 函數。main() 會調用 UIApplicationMain()。
改善啓動時間
從點擊 App 圖標到加載 App 閃屏之間會有個動畫,咱們但願 App 啓動速度比這個動畫更快。雖然不一樣設備上 App 啓動速度不同,但啓動時間最好控制在 400ms。須要注意的是啓動時間一旦超過 20s,系統會認爲發生了死循環並殺掉 App 進程。固然啓動時間最好以 App 所支持的最低配置設備爲準。直到 applicationWillFinishLaunching 被調動,App 才啓動結束。
測量啓動時間
Warm launch: App 和數據已經在內存中
Cold launch: App 不在內核緩衝存儲器中
冷啓動(Cold launch)耗時纔是咱們須要測量的重要數據,爲了準確測量冷啓動耗時,測量前須要重啓設備。在 main() 方法執行前測量是很難的,好在 dyld 提供了內建的測量方法:在 Xcode 中 Edit scheme -> Run -> Auguments 將環境變量DYLD_PRINT_STATISTICS 設爲 1。控制檯輸出的內容以下:
1
2
3
4
5
6
7
8
|
Total pre-main time: 228.41 milliseconds (100.0%)
dylib loading time: 82.35 milliseconds (36.0%)
rebase/binding time: 6.12 milliseconds (2.6%)
ObjC setup time: 7.82 milliseconds (3.4%)
initializer time: 132.02 milliseconds (57.8%)
slowest intializers :
libSystem.B.dylib : 122.07 milliseconds (53.4%)
CoreFoundation : 5.59 milliseconds (2.4%)
|
優化啓動時間
能夠針對 App 啓動前的每一個步驟進行相應的優化工做。
加載 Dylib
以前提到過加載系統的 dylib 很快,由於有優化。但加載內嵌(embedded)的 dylib 文件很佔時間,因此儘量把多個內嵌 dylib 合併成一個來加載,或者使用 static archive。使用 dlopen() 來在運行時懶加載是不建議的,這麼作可能會帶來一些問題,而且總的開銷更大。
Rebase/Binding
以前提過 Rebaing 消耗了大量時間在 I/O 上,而在以後的 Binding 就不怎麼須要 I/O 了,而是將時間耗費在計算上。因此這兩個步驟的耗時是混在一塊兒的。
以前說過能夠從查看 __DATA 段中須要修正(fix-up)的指針,因此減小指針數量纔會減小這部分工做的耗時。對於 ObjC 來講就是減小 Class,selector 和 category 這些元數據的數量。從編碼原則和設計模式之類的理論都會鼓勵你們多寫精緻短小的類和方法,並將每部分方法獨立出一個類別,其實這會增長啓動時間。對於 C++ 來講須要減小虛方法,由於虛方法會建立 vtable,這也會在__DATA 段中建立結構。雖然 C++ 虛方法對啓動耗時的增長要比 ObjC 元數據要少,但依然不可忽視。最後推薦使用 Swift 結構體,它須要 fix-up 的內容較少。
ObjC Setup
針對這步所能事情不多,幾乎都靠 Rebasing 和 Binding 步驟中減小所需 fix-up 內容。由於前面的工做也會使得這步耗時減小。
Initializer
顯式初始化
使用 +initialize 來替代 +load
不要使用 __atribute__((constructor)) 將方法顯式標記爲初始化器,而是讓初始化方法調用時才執行。好比使用dispatch_once(),pthread_once() 或 std::once()。也就是在第一次使用時才初始化,推遲了一部分工做耗時。
隱式初始化
對於帶有複雜(non-trivial)構造器的 C++ 靜態變量:
在調用的地方使用初始化器。
只用簡單值類型賦值(POD:Plain Old Data),這樣靜態連接器會預先計算 __DATA 中的數據,無需再進行 fix-up 工做。
使用編譯器 warning 標誌 -Wglobal-constructors 來發現隱式初始化代碼。
使用 Swift 重寫代碼,由於 Swift 已經預先處理好了,強力推薦。
不要在初始化方法中調用 dlopen(),對性能有影響。由於 dyld 在 App 開始前運行,因爲此時是單線程運行因此係統會取消加鎖,但 dlopen() 開啓了多線程,系統不得不加鎖,這就嚴重影響了性能,還可能會形成死鎖以及產生未知的後果。因此也不要在初始化器中建立線程。
Reference:https://developer.apple.com/videos/play/wwdc2016/406/