天天看點

iOS開發-main函數之前app做了哪些事main之前啟動時間優化二進制重排

文章目錄

  • 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

檔案的集合)

Mach-O

是針對不同運作時可執行檔案的檔案類型。對于

Fat

檔案來說,可以拆分為各個架構的

thin

檔案,

thin

檔案再進行解包就是

.o

檔案,對一個

.o

檔案進行

file

指令我們會看到如下:

prep_cif.o: Mach-O 64-bit object arm64
           
如果對

Fat

檔案,

Thin

檔案概念比較模糊看 這裡

使用

MachOView

進行檢視,其結構如下

iOS開發-main函數之前app做了哪些事main之前啟動時間優化二進制重排

這是一個單個

Object

,一般來說我們會直接檢視某個架構的

.a

檔案。

幾乎所有

Mach-O

都包含這三個

段(segment)

__TEXT

,

__DATA

__LINKEDIT

  • __TEXT

    包含

    Mach header

    ,被執行的代碼和隻讀常量(如

    C 字元串

    )。

    隻讀可執行(r-x)

  • __DATA

    包含全局變量,靜态變量等。

    可讀寫(rw-)

  • __LINKEDIT

    包含了加載程式的

    『中繼資料』

    ,比如函數的名稱和位址。

    隻讀(r–)

我們可以檢視app程式下的二進制檔案

iOS開發-main函數之前app做了哪些事main之前啟動時間優化二進制重排
  • 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

    檔案可能會再依賴其他

    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

:将指針指向鏡像外部的内容

可以通過

MachOView

檢視

rebase

bind

等資訊,下圖可以看到我們常用的

NSLog

iOS開發-main函數之前app做了哪些事main之前啟動時間優化二進制重排

我們使用

PIC

的概念和

fishhook

就可以進行

hook

外部

dylib

的函數,例如

NSLog

,見 部落格

  • Rebasing

    在過去,會把

    dylib

    加載到指定位址,所有指針和資料對于代碼來說都是對的,

    dyld

    就無需做任何

    fix-up

    了。如今用了

    ASLR

    後悔将

    dylib

    加載到新的

    随機位址(actual_address)

    ,這個随機的位址跟代碼和資料指向的

    舊位址(preferred_address)

    會有偏差,dyld

    需要修正這個偏差(

    slide

    ),做法就是将 dylib 内部的指針位址都加上這個偏移量,偏移量的計算方法如下:

ASLR

(Address Space Layout Randomization):位址空間布局随機化,鏡像會在随機的位址上加載。
slide = actual_address - preferred_address
           
  • Binding

    Binding

    是處理那些指向 dylib 外部的指針,它們實際上被符号

    (symbol)

    名稱綁定,也就是個字元串。之前提到

    __LINKEDIT

    段中也存儲了需要

    bind

    的指針,以及指針需要指向的符号。dyld

    需要找到

    symbol

    對應的實作,這需要很多計算,去符号表裡查找。找到後會将内容存儲到

    __DATA

    段中的那個指針中。

    Binding

    看起來計算量比

    Rebasing

    更大,但其實需要的 I/O 操作很少,因為之前 Rebasing 已經替 Binding 做過了。

3.Objc 運作時的初始處理,包括 Objc 相關類的注冊、category 注冊、selector 唯一性檢查等;

  • ObjC Runtime

    Objective-C

    中有很多資料結構都是靠

    Rebasing

    Binding

    (fix-up)

    的,比如 Class 中

    指向元類的指針

    指向方法的指針

    ObjC

    是個動态語言,可以用類的名字來執行個體化一個

    類的對象

    。這意味着

    ObjC Runtime

    需要維護一張

    映射類名與類的全局表

    。當加載一個

    dylib

    時,其定義的

    所有的類

    都需要被注冊到這個全局表中。

    C++

    中有個問題叫做

    易碎的基類(fragile base class)

    ObjC

    就沒有這個問題,因為會在加載時通過

    fix-up

    動态類中改變執行個體變量的偏移量。
易碎的基類:如果一個程式員無論何時修改了一個類,無論修改的是公共接口部分還是私有成員的聲明部分,他都必須再次編譯包含頭檔案的所有檔案,這就是易碎的基類問題。因為其成員的偏移量有改變,而ObjC會進行

fix-up

來修複執行個體變量的偏移。

ObjC

中可以通過

定義類别(Category)

的方式改變一個

的方法。有時你想要添加方法的類在另一個 dylib 中,而不在你的鏡像中(也就是對系統或别人的類動刀),這時也需要做些

fix-up

ObjC

中的

selector

必須是唯一的。

  1. 去除不必要的類,可以通過

    infer

    等編譯時工具進行檢測,去掉不必要的代碼,減少庫

    image

    mapped

  2. 是以減少

    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

    App

    需要使用同一部分代碼時,我們需要将其封裝動态庫 (See this blog)。動态庫同時也能解決二進制封包件過大的問題,蘋果對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

就像女人關注你的細節一樣😅,細節重要,可不要忘了決定成敗的是你本身🙄