我們日常開發的打包或者 SDK 的打包會生成一個ipa 或者 framework。在 framework 和 ipa 文件中其實都可以找到一個 exec 文件。這個文件就是一個 Mach-O 文件。這一次主要就是深入的去了解 Mach - O 文件在到底都用來做什么。
(一)了解 Mach - O 的結構
如果我們想對 Mach -O 文件有所了解,可以將我們打包好的 ipa 文件后綴改成 .zip,然后解壓生成 Payload 文件,在其中就可以找到 exec 文件。或者找一個動態庫的 framework 在其中也可以找到 exec 文件。
然后用 MachOView 獲取文件內容。MachOView 相關教程
文件格式大致如下。
1.Fat Header 文件
在 MachOView
中查看 Fat Header
結構大概如下圖
PS:上下兩個圖使用了不一樣的 exec 文件 因為我的 MachOView 一直閃退... 知道好的解決方案的小伙伴也煩請告知
Magic Number
主要是快速的獲取當前的二進制文件用于 32 位還是 64 位CPU
從中我們同時可知這個二進制文件支持的架構個數。如果想知道 framework
是否存在隱患,不支持你需要支持機型的架構,你提前就可以這樣進行查看。
同時可知如果我們的 ipa 打包好后,下發給用戶,如圖Mach-O 1.0
可知文件中包含多個所支持架構生成的文件。也是說使用Fat Header
讀取來獲取與當前 CPU 匹配的 Executable
,然后在進行后續的操作。當然如果我們是制作 SDK , 此時就是生成一個 Library
。
接下來就來探究他們的結構。
2.Executable 和 Library
打開后可以看到其架構結構大致如下
Mach Header
的結構如下
其實這和上邊的
Fat Header
很相似,但是這里主要包含下文會介紹的加載過程中的信息(比如 SEGMENT
段中需要加載的 dyld 信息就是由 Mach Header
提供)
現在看看 Load Commands
,這里就是二進制文件加載進內存要執行的一些指令。
這里的指令主要在負責我們 APP 對應進程的創建和基本設置(分配虛擬內存,創建主線程,處理代碼簽名/加密的工作),然后對動態鏈接庫(.dylib 系統庫和我們自己創建的動態庫)進行庫加載和符號解析的工作。
首先看下 Load Commands
的目錄結構
從上圖可知 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()
函數調用前都做了什么,那么我們就可以優化這一部分的執行時間。
測試啟動時間可以如下設置
用我們的項目測了下啟動時間,大致如下。
從上圖可知項目的啟動時間,就從上邊各個階段的原理上去找尋優化方案。
-
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 操作系統一書。