文章目錄
- main之前
-
- 1.加載可執行檔案(App 的`.o `檔案的集合)
- 2.加載動态連結庫,進行 rebase 指針調整和 bind 符号綁定;
- 3.Objc 運作時的初始處理,包括 Objc 相關類的注冊、category 注冊、selector 唯一性檢查等;
- 4.初始化,包括了執行 +load() 方法、attribute((constructor)) 修飾的函數的調用、建立 C++ 靜态全局變量。
- 啟動時間優化
- 二進制重排
一般情況下,App 的啟動分為
冷啟動
和
熱啟動
。
冷啟動
是指,
App
點選啟動前,它的程序不在系統裡,需要系統新建立一個
程序
配置設定給它啟動的情況。這是一次完整的啟動過程。
熱啟動
是指 ,
App
在
冷啟動
後使用者将 App 退背景,在 App 的程序還在系統裡的情況下,使用者重新啟動進入 App 的過程,這個過程做的事情非常少。
main之前
在
main() 函數
執行前,系統主要會做下面幾件事情:
1.加載可執行檔案(App 的 .o
檔案的集合)
.o
Mach-O
是針對不同運作時可執行檔案的檔案類型。對于
Fat
檔案來說,可以拆分為各個架構的
thin
檔案,
thin
檔案再進行解包就是
.o
檔案,對一個
.o
檔案進行
file
指令我們會看到如下:
prep_cif.o: Mach-O 64-bit object arm64
如果對檔案,
Fat
檔案概念比較模糊看 這裡
Thin
使用
MachOView
進行檢視,其結構如下
這是一個單個
Object
,一般來說我們會直接檢視某個架構的
.a
檔案。
幾乎所有
Mach-O
都包含這三個
段(segment)
:
__TEXT
,
__DATA
和
__LINKEDIT
:
-
包含__TEXT
,被執行的代碼和隻讀常量(如Mach header
)。C 字元串
。隻讀可執行(r-x)
-
包含全局變量,靜态變量等。__DATA
。可讀寫(rw-)
-
包含了加載程式的__LINKEDIT
,比如函數的名稱和位址。『中繼資料』
。隻讀(r–)
我們可以檢視app程式下的二進制檔案
-
檔案Mach-O Universal
檔案,将多種架構的FAT 二進制
檔案合并而成。它通過Mach-O
Fat Header
來記錄不同架構在檔案中的偏移量,Fat Header 占一頁的空間。
按分頁來存儲這些
和segement
會浪費空間,但這有利于header
的實作。虛拟記憶體
2.加載動态連結庫,進行 rebase 指針調整和 bind 符号綁定;
下面的步驟構成了 dyld 的時間線:
Load dylibs -> Rebase -> Bind -> ObjC -> Initializers
- 加載
從主執行檔案的Dylib
擷取到需要加載的所依賴動态庫清單,而 header 早就被核心映射過。然後它需要找到每個header
,然後打開檔案讀取檔案起始位置,確定它是dylib
檔案。接着會找到代碼簽名并将其注冊到核心。然後在Mach-O
檔案的每個dylib
上調用segment
。應用所依賴的mmap()
檔案可能會再依賴其他dylib
,是以 dyld 所需要加載的是動态庫清單一個遞歸依賴的集合。一般應用會加載dylib
到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
:将指針指向鏡像外部的内容
可以通過
MachOView
檢視
rebase
和
bind
等資訊,下圖可以看到我們常用的
NSLog
我們使用
PIC
的概念和
fishhook
就可以進行
hook
外部
dylib
的函數,例如
NSLog
,見 部落格
-
在過去,會把Rebasing
加載到指定位址,所有指針和資料對于代碼來說都是對的,dylib
就無需做任何dyld
了。如今用了fix-up
後悔将ASLR
加載到新的dylib
,這個随機的位址跟代碼和資料指向的随機位址(actual_address)
舊位址(preferred_address)
會有偏差,dyld
需要修正這個偏差(
),做法就是将 dylib 内部的指針位址都加上這個偏移量,偏移量的計算方法如下:slide
ASLR
(Address Space Layout Randomization):位址空間布局随機化,鏡像會在随機的位址上加載。
slide = actual_address - preferred_address
-
Binding
是處理那些指向 dylib 外部的指針,它們實際上被符号Binding
名稱綁定,也就是個字元串。之前提到(symbol)
段中也存儲了需要__LINKEDIT
bind
的指針,以及指針需要指向的符号。dyld
需要找到
對應的實作,這需要很多計算,去符号表裡查找。找到後會将内容存儲到symbol
段中的那個指針中。__DATA
看起來計算量比Binding
更大,但其實需要的 I/O 操作很少,因為之前 Rebasing 已經替 Binding 做過了。Rebasing
3.Objc 運作時的初始處理,包括 Objc 相關類的注冊、category 注冊、selector 唯一性檢查等;
-
ObjC Runtime
中有很多資料結構都是靠Objective-C
和Rebasing
來Binding
的,比如 Class 中(fix-up)
和指向元類的指針
。指向方法的指針
是個動态語言,可以用類的名字來執行個體化一個ObjC
。這意味着類的對象
需要維護一張ObjC Runtime
。當加載一個映射類名與類的全局表
時,其定義的dylib
都需要被注冊到這個全局表中。所有的類
中有個問題叫做C++
。易碎的基類(fragile base class)
就沒有這個問題,因為會在加載時通過ObjC
動态類中改變執行個體變量的偏移量。fix-up
易碎的基類:如果一個程式員無論何時修改了一個類,無論修改的是公共接口部分還是私有成員的聲明部分,他都必須再次編譯包含頭檔案的所有檔案,這就是易碎的基類問題。因為其成員的偏移量有改變,而ObjC會進行 fix-up
來修複執行個體變量的偏移。
在
ObjC
中可以通過
定義類别(Category)
的方式改變一個
類
的方法。有時你想要添加方法的類在另一個 dylib 中,而不在你的鏡像中(也就是對系統或别人的類動刀),這時也需要做些
fix-up
。
ObjC
中的
selector
必須是唯一的。
- 去除不必要的類,可以通過
等編譯時工具進行檢測,去掉不必要的代碼,減少庫infer
的image
mapped
- 是以減少
的數量也是能夠一定量上提高category
的運作速度app
4.初始化,包括了執行 +load() 方法、attribute((constructor)) 修飾的函數的調用、建立 C++ 靜态全局變量。
-
+load()
會分别執行每個
類
和
分類
的
+load
方法
-
attribute((constructor))
#include <stdio.h>
#include <stdlib.h>
void static __attribute__((constructor)) before_main()
{
printf("before main\n");
}
void static __attribute__((destructor)) after_main()
{
printf("after main\n");
}
int main(int argc, char** argv)
{
printf("hello world!\n");
}
__attribute__((constructor))
修飾的函數在
main
函數之前執行
__attribute__((destructor))
修飾的函數在
main
函數之後執行
-
在類加載之前被初始化,限于類中使用。C++ 靜态全局變量
啟動時間優化
Xcode
為我們提供了擷取各個
dylib
加載時間的方法,在
Xcode
中 Edit
scheme -> Run -> Auguments
将環境變量
DYLD_PRINT_STATISTICS
設為 1
相應地,這個階段對于啟動速度優化來說,可以做的事情包括:
- 減少動态庫加載。每個庫本身都有依賴關系,蘋果公司建議使用更少的動态庫。靜态庫的加載時間更短,但是當我們的
和Extension
需要使用同一部分代碼時,我們需要将其封裝動态庫 (See this blog)。動态庫同時也能解決二進制封包件過大的問題,蘋果對app包大小判斷是不将動态庫包大小計算在内的。App
-
應設為check framework
和optional
,如果該required
在目前framework
支援的所有App
系統版本都存在,那麼就設為iOS
,否則就設為required
,因為optional
會有些額外的檢查;optional
- 減少加載啟動後不會去使用的類或者方法。
- 合并或者删減一些
類,關于清理項目中沒用到的類,可以借助OC
代碼檢查工具:AppCode
- 删減一些無用的靜态變量
- 删減沒有被調用到或者已經廢棄的方法
- 盡量不要用
虛函數(建立虛函數表有開銷)C++
- 避免使用
,可将要實作的内容放在初始化方法中配合attribute((constructor))
使用。dispatch_once
-
方法裡的内容可以放到首屏渲染完成後再執行,或使用+load()
方法替換掉。因為,在一個+initialize()
方法裡,進行運作時方法替換操作會帶來+load()
的消耗。不要小看這4 毫秒
,積少成多,執行4 毫秒
方法對啟動速度的影響會越來越大。+load()
- 控制
全局變量的數量。C++
二進制重排
二進制重排是虛拟記憶體基于頁和段來加載讀取的原理,抖音團隊對二進制重排有一篇文章進行了講解 抖音研發實踐:基于二進制檔案重排的解決方案 APP啟動速度提升超15%
參考文章:
https://www.jianshu.com/p/534a37f588f2
https://www.jianshu.com/p/54d842db3f69
https://blog.automatic.com/how-we-cut-our-ios-apps-launch-time-in-half-with-this-one-cool-trick-7aca2011e2ea
就像女人關注你的細節一樣😅,細節重要,可不要忘了決定成敗的是你本身🙄