理解 Mach O 並提高程式啟動速度

Edgars發表於2017-12-13

我們日常開發的打包或者 SDK 的打包會生成一個ipa 或者 framework。在 framework 和 ipa 檔案中其實都可以找到一個 exec 檔案。這個檔案就是一個 Mach-O 檔案。這一次主要就是深入的去了解 Mach - O 檔案在到底都用來做什麼。

(一)瞭解 Mach - O 的結構

如果我們想對 Mach -O 檔案有所瞭解,可以將我們打包好的 ipa 檔案字尾改成 .zip,然後解壓生成 Payload 檔案,在其中就可以找到 exec 檔案。或者找一個動態庫的 framework 在其中也可以找到 exec 檔案。

然後用 MachOView 獲取檔案內容。MachOView 相關教程 檔案格式大致如下。

Mach-O 1.0

1.Fat Header 檔案

MachOView 中檢視 Fat Header結構大概如下圖 PS:上下兩個圖使用了不一樣的 exec 檔案 因為我的 MachOView 一直閃退... 知道好的解決方案的小夥伴也煩請告知

Mach-O 1.1

Magic Number 主要是快速的獲取當前的二進位制檔案用於 32 位還是 64 位CPU

從中我們同時可知這個二進位制檔案支援的架構個數。如果想知道 framework 是否存在隱患,不支援你需要支援機型的架構,你提前就可以這樣進行檢視。

同時可知如果我們的 ipa 打包好後,下發給使用者,如圖Mach-O 1.0可知檔案中包含多個所支援架構生成的檔案。也是說使用Fat Header讀取來獲取與當前 CPU 匹配的 Executable,然後在進行後續的操作。當然如果我們是製作 SDK , 此時就是生成一個 Library 。 接下來就來探究他們的結構。

2.Executable 和 Library

開啟後可以看到其架構結構大致如下

Mach-O 1.2

Mach Header 的結構如下

Mach-O 1.3
其實這和上邊的 Fat Header 很相似,但是這裡主要包含下文會介紹的載入過程中的資訊(比如 SEGMENT 段中需要載入的 dyld 資訊就是由 Mach Header 提供)

現在看看 Load Commands ,這裡就是二進位制檔案載入進記憶體要執行的一些指令。 這裡的指令主要在負責我們 APP 對應程式的建立和基本設定(分配虛擬記憶體,建立主執行緒,處理程式碼簽名/加密的工作),然後對動態連結庫(.dylib 系統庫和我們自己建立的動態庫)進行庫載入和符號解析的工作。

首先看下 Load Commands 的目錄結構

Mach-O 1.4

從上圖可知 Load Commands 主要包含了有多個 Segment 段,每個中又包含了多個 Section 段。每一部分都是系統執行指令。 其中 LC_SEGMENT 包含空指標陷阱 __TEXT 段主要包含程式程式碼和只讀的常量,這個段的內容如果是系統動態庫的內容那麼所有程式公用 __DATA 段主要包含全域性變數和靜態變數,這個段的內容每個程式單獨進行維護 __LINKEDIT 主要包含連結器使用的符號和其他的表(比如函式名稱、地址等) 這個段的內容也是可以多程式公用的。

此外還需介紹下和 SEGMENT 並列的一些比較重要的指令。

LC_MAIN 是在所有的庫都載入完成後,有其中的指令啟動程式的主執行緒。我們的程式也是在這個函式之後才開始執行 main() 函式的。

LC_CODE_SIGNATURE 我想每個 iOSer 都知道程式碼簽名的機制,其實程式碼簽名的校驗也是在這個指令下進行。實際上指令會把整個檔案進行 hash 化處理並簽名,在執行時去驗證簽名的正確性。(想要詳細瞭解程式碼簽名機制的小夥伴看這裡)

#(二)Mach - O 載入過程

我們在瞭解了 Mach-O 的結構後再看載入過程應該更好理解一些。 Mach-O 的載入的過程大致如下

  • load dyld

PS:在 iOS 10 後 dyld 為 tbd,網上有說法 tbd 的出現是因為 iOS 10 後對系統檔案進行壓縮後的檔案就是現在的 tbd , 能起到減少包大小的作用。

dyld 載入階段主要是載入動態連結庫的過程,所要載入的 dyld 在上文中的 Mach Header 中有記錄,這樣就知道了檔案的讀取位置,然後進行程式碼簽名並註冊進核心。但是當前載入的 dyld 可能會包含其他 dyld ,所以這是就需要遞迴的進行載入。MAC OS 和 iOS 中都有共享快取庫的概念,一般都把 dyld 進行預先連結,然後將連結儲存在一個磁碟上,這樣對於這一部分的載入速度會很快。一般應用載入的 dyld 在 100 ~ 400 個左右。

  • Rebasing

因為當前系統記憶體空間地址佈局的隨機化,所有現在讀取 dyld 之後載入到的地址的都是隨機化的,這就和程式碼以及資料指向的舊地址有偏差, 在這個過程中主要做的就是修復這個隨機化的地址。

  • Binding

簡單的解釋,就是我們在呼叫 dyld 的過程中可能會插入自己的程式碼,在上一步中我們修復了 dyld 的指標地址,但是在 __LINKEDIT 中對於我們自己寫的程式碼是用符號(symbol)進行繫結的。這個時候就需要找到指標指向的符號以及符號的具體的實現,然後進行 bind 的過程,這時候就去符號表中進行查詢,找到後儲存到 __DATA 段中的那個指標中,保證程式執行時可以正確的 jump 到正確的指令處 。

  • Objc Setup

這個過程如下: 1.類註冊的過程,然後維護一張對映類名和類的全域性表。 2.對 Category 中的定義的方法,協議等插入對應的方法,協議等列表。 3.確定類方法的唯一性。

  • Initializers

這裡主要對於 OC 物件回撥用每個類的 +load 方法。 對於類物件的呼叫順序是 根據之前 dylib 載入行程了一張巨大的網,現在從子節點一直向上載入到根節點。 這樣確保 dyld 載入前依賴的 dyld 已經載入。

上邊一些列步驟執行結束之後會執行我們程式中的 main() 函式,然後執行 APPDelegate中的函式。

(三)改善啟動時間

在瞭解了 Mach-O 檔案的原理之後,那麼我們能做些什麼呢?其實我們已經知道了 main() 函式呼叫前都做了什麼,那麼我們就可以優化這一部分的執行時間。

測試啟動時間可以如下設定

Mach-O 3.1

用我們的專案測了下啟動時間,大致如下。

Mach-O 3.2

從上圖可知專案的啟動時間,就從上邊各個階段的原理上去找尋優化方案。

  • load dyld images

上文已說蘋果對於這部分的優化已經做了共享快取庫,如果有部分內嵌(embedded)庫,這一部分的載入時間可能會較慢,現有方案就是將這一部分的庫進行組合或者使用靜態連結庫進行解決。記得去年聽 devLink 的時候小虎哥說過一些場景下用靜態庫會出現問題,他們最後的解決方案可以參考這篇文章

  • rebase & bind

對於 OC 而言,這一部分主要就是減少地址隨機化的修正的過程和符號定址的過程,實際應用中減少 Class ,Selector 和 Category 的數量。

  • ObjC Setup

這一部分可優化空間。這裡出現的問題,其實和 rebase & bind 中的問題類似,其實還是需要減少 Class 、Category、Selector 的數量。

  • ###Initializer 因為 + load 方法在這個過程中呼叫,所以呼叫 +load 的方法最好改成 +initialize

現在我們對 Mach - O 就有了一定的理解。此時對於 Mach-O 檔案的生成過程比較感興趣,接下來的文章可能會關於編譯過程文章閱讀後的總結和理解。

本文在書寫過程中參考了國內大牛們的優秀文章。 參考文章如下: 楊蕭玉的文章 今日頭條技術部落格 蘋果去年的WWDC 南梔傾寒的簡書 深入解析 MAC OS X & iOS 作業系統一書。

相關文章