優(yōu)化應用的啟動時間(理論篇)

這是 WWDC 2016 Session 406 理論部分的筆記,內容包含著了 Mach-O,虛擬內存的一點點知識,不過主要還是關注在 main() 函數(shù)之前做了什么。

Mach-O

Mach-O 是一種運行時可執(zhí)行二進制文件類型。除了在應用中常見的可執(zhí)行文件之外,還有 dylib(動態(tài)庫),bundle(特殊的 dylib,只能在運行時通過 dlopen() 函數(shù)裝載)等等,都是 Mach-O 文件,也被稱作 Image。這些文件運行在那些基于 Mach 內核的操作系統(tǒng)上,比如 macOS 和 iOS 等等。

Mach-O 文件的結構

Mach-O_Format.png

Mach-O 被分為不同的 segment,每個 segment 的大小都是頁面大小(page size)的倍數(shù)(arm64 環(huán)境下 page size = 16KB,其它為 4K)。常見的有 __TEXT, __DATA, __LINKEDIT。通過 otool -tV -d 可以讀取對象文件的 __TEXT 段和 __DATA 段的內容:

  1. __TEXT segment 包含 Mach 頭文件、代碼以及只讀常量;
  2. __DATA segment 包含有可讀的內容,如全局變量、靜態(tài)變量等等;
  3. __LINKEDIT segment 包含有如何裝載程序的元數(shù)據(jù)(meta data)。

Universal Binary

Univseral_Files.png

Universal Binary 最早在 WWDC 2005 上被提出,目的是幫助 OS X 上的應用從基于 PowerPC 架構到 Intel 架構的轉變。即可執(zhí)行文件中包含著多種指令集,不同的系統(tǒng)可以根據(jù) Mach-O 上的 Fat Header 上的信息選擇執(zhí)行相應的指令,副作用是使得可執(zhí)行文件的體積增大。

比如將 Build Setting -> Valid Architectures 中的 armv7 和 armv7s 刪除,僅剩 arm64,那么導出的可執(zhí)行文件的體積會小很多(除非應用僅支持64位處理器,否則不要這么做)。

Size_Cmp_Diff_Arch.png

虛擬內存

虛擬內存是一種將進程虛擬地址空間映射到物理地址的一種機制。使用虛擬地址的好處是使得程序空間獨立。一般的操作系統(tǒng)在實現(xiàn)虛擬內存的時候,都會將內存空間劃分為頁(page),通過頁表(page table)去管理虛擬頁和物理頁之間的映射關系和 page-in & page-out 。

ASLR

地址空間布局隨機化(Address Space Layout Randomization),即將可執(zhí)行文件、動態(tài)庫等文件隨機地裝載到內存的某個地址中防止緩沖區(qū)溢出攻擊。

Code Signing

每一個頁的內容都被加密散列,散列值存放在 __LINKEDIT segment 中。在 page-in 的時候會檢驗內容的正確性,這意味著程序的指令不會被修改。

從 exec() 到 main()

在執(zhí)行 main() 函數(shù)之前,大概經(jīng)過了這些步驟:

  • 運行輔助程序 dyld(與進程在同一地址空間);
  • dyld 裝載所有程序依賴的動態(tài)庫;
  • dyld 修正 __DATA segment 內的數(shù)據(jù)指針;
  • 調用所有 initializers.

Loading Dylibs :

動態(tài)庫(或者叫共享庫,通常是 .so .dylib 作為后綴)是一種可以在程序在運行時裝載的目標文件。使用動態(tài)庫可以有效減少代碼的體積,提高代碼的復用率。下面是 iOS 在運行前加載動態(tài)庫的過程:

  1. 解析程序所依賴的 dylibs;
  2. 找到所需的 Mach-O 文件;
  3. 打開并讀取文件;
  4. 校驗 Mach-O 文件;
  5. 向內核注冊代碼簽名;
  6. 對每個 segment 調用 mmap(),將目標文件映射到內存中。

上面的這個過程是遞歸進行的,因為一個動態(tài)庫可能還依賴著另一個動態(tài)庫。

Rebasing

Rebasing 的工作是改變 dylibs 或者 bundles 的基地址?;刂肥?image(dylib 或者 bundle)在被裝載時優(yōu)先選擇的地址,被編碼到 __LINKEDIT 中,默認為零。在運行時,如果基地址范圍被占用了,那么 dyld 會將這個 image 裝載到一個新的地址空間去(內容來源于 rebase 的 manual,在 terminal 里敲入 man rebase 就能看到)。

Binding

不同于 rebasing(修正 dylib 內部每個指向 image 內部的地址),binding 是要修正所有指向其它 dylib 指針的值。比如說在程序中調用 malloc 函數(shù),dyld 需要在共享庫中找到 malloc 這個符號對應的子程序的地址然后修改調用程序中的指針。

這里有個命令可以查看可執(zhí)行文件中的 dyld 信息:
xcrun dyldinfo -rebase -bind -lazy_bind YourExecutableFile

Notify ObjC Runtime

在 rebasing 和 binding 結束之后,ObjC runtime 初始化,接著通過 class_createInstance()注冊類到 runtime 中。類的成員變量的偏移量隨之更新,category 上的方法也被插入到方法列表中。

Initializers

之后調用 ObjC 中類的 +load 方法。視頻中 Nick Kledzik 說 +load 這個方法已經(jīng) deprecated 了,不建議使用,但是去 NSObject Class Reference 看,并沒有對此做了什么標記。接著調用 __attribute__((constructor)) 修飾的函數(shù),然后就是對 C++ 中全局對象進行初始化(如下圖,Person 的構造函數(shù)先于程序的 main() 函數(shù)的調用)。

static_cpp_object.png

結束

至此可知,在 main() 函數(shù)之前,我們可以通過減少庫的數(shù)目、減少類的數(shù)目、不在+load里面做過多的事情等手段,加快應用的啟動速度。在 main() 函數(shù)之后,就是要求主線程在 -application:didFinishLaunchingWithOptions: 中盡快返回。

上述的每一個步驟都可以展開很多內容來講,這里推薦一些比較厲害的博客,sunnyxx 的《iOS 程序 main 函數(shù)之前發(fā)生了什么》 還有 mikeash.com 上的《Friday Q&A 2012-11-09: dyld: Dynamic Linking On OS X》。

我才不會說我把這個視頻的字幕翻譯了一遍才勉強看得懂咧……??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容