美人相機啟動優化

wolfcolony發表於2018-01-15

前言

去年年底針對美人相機啟動緩慢做了一次調研和優化,這周有空抽空整理一下,從自己針對這次美人相機啟動的調研、實施經歷做一次總結以及學習的記錄。

文章主要從三個部分展開,針對 WWDC 疑惑的地方進行的一系列探討(畢竟喜歡鑽牛角尖),針對美人相機進行的專案分析,以及美人相機的優化和總結,文章內容可能相對比較雜。

關於 APP 啟動的理論

關於 APP 啟動時間,主要由 pre-main 和 main 之後的時間組成,即總的啟動時間 = main 之前載入的時間 + main 之後載入的時間,所以接下來分兩部分展開介紹。

啟動時間越接近 400ms 越好,並且最好控制在 20s 以內,不然系統會以為 APP 進入一個死迴圈,應用程式將會被系統強制殺除。

pre-main 載入時間

pre-main 之前沒有具體的工具可以進行測量,所以比較棘手,為此蘋果針對 iOS 10 之後的機器提供了 DYLD_PRINT_STATISTICS 的環境變數的支援,只需要在 scheme 中新增 DYLD_PRINT_STATISTICS 並且開啟即可在控制檯輸出 pre-main 載入時間:

新增 DYLD_PRINT_STATISTICS

專案開發中最好一直開啟 DYLD_PRINT_STATISTICS,比如某部分需求完成之後發現 pre-main 提升了不少時間,可以重點對這部分進行一次 review

pre-main 載入時間列印

執行的時候就可以列印出詳細的 pre-mian 的時間:

pre-main 時間

可以直觀的看到 pre-main 之前的時間由 dylib loadingrebase / bindingObjC setupinitializer 四部分耗時構成。

熱啟動和冷啟動

每次啟動的 pre-main 的時間都是存在波動的,並且還有一個熱啟動和冷啟動的概念,冷啟動即 APP 的一些資料還沒有在核心快取中,一般是第一次啟動,或者很久沒有啟動 APP 核心快取已經被其他 APP 佔用更新,熱啟動指的是 APP 的一些資料已經被核心快取過,所以熱啟動 pre-main 時間比冷啟動 pre-main 時間較短。

Mach-O

理解 APP 啟動詳細過程,瞭解 Mach-O 是不可或缺的一部分了,蘋果 WWDC 介紹啟動優化的 seesion 中也有做出介紹,在沒有接觸到啟動優化的時候,其實對 Mach-O 有點陌生,在 Mac 上我們可以使用 file 命令檢視檔案的型別,不同檔案雖然都屬於 Mach-O 這種格式,但是他們具體的型別卻不一樣:

使用 file 命令檢視檔案型別

所以關於 Mach-O 到底是個什麼玩意,查詢了蘋果 Mach-O ABI 文件瞭解了一波,以下是蘋果文件原文,簡單的理解就是 Mach-O 是 a.out 格式的一個更加靈活的替代品:

The Mach-O file format provides both intermediate (during the build process) and final (after linking the final product) storage of machine code and data. It was designed as a flexible replacement for the BSD a.out format, to be used by the compiler and the static linker and to contain statically linked executable code at runtime. Features for dynamic linking were added as the goals of OS X evolved, resulting in a single file format for both statically linked and dynamically linked code.

Mach-O 格式的檔案,基本結構都由 Header、Load commands 和 Data 三部分構成:

Mach-O 基本結構

關於 Mach-O 的結構,可以用命令檢視,也可以用工具,快速檢視那就推薦特別好用的 MachOView,將工程編譯找到沙盒檔案然後用 MachOView 檔案開啟程式的二進位制檔案即可:

使用 MachOView 檢視 Mach-O 結構

Header 是一個結構體可以在蘋果開源的核心原始碼中找到,這樣看起來會更直觀,Header 可以指定目標架構,比如是在 arm64 還是 x86 上,用於核心驗證,保證平臺的正確性等等:

mach_header 結構體

Load Commands 用於指定佈局和檔案的連結特性,符號表的位置,動態連結器路徑等等,Load Commands 指令個數和總的佔用大小在 Mach Header 中已經被指定,在 mach_header 的 ncmds 和 sizeofcmds 欄位可以檢視,Load Commands 由多個 Load Command 構成,每一個 Load Command 也是一個結構體,並且不同的 Load Command 對應的結構體結構也不相同,但是所有的 Load Command 都必須存在的資訊是指令型別 cmd 和指令大小 cmdsize:

load_command 結構體

在 MachOView 中點開 Load Commands 檢視:

load_command 結構體

__PAGEZERO、__TEXT、__DATA、__LINKEDIT 載入指令都是 LC_SEGMENT,這些位元組被對映到虛擬記憶體,所以段和虛擬記憶體頁面是位元組對齊的(其他的型別可以自己原始碼中檢視):

LC_SEGMENT 解釋

Load Commands 中關於對映的段的詳細資訊都可以在 MachOView 中檢視到,這些是一一對應的:

MachOView 顯示圖

可以清楚的看到,__TEXT、__DATA、__LINKEDIT 段名都是大寫的,並且每個段被劃分成 0 或者多個 section,section 名都是小寫的,例如 __text,__stubs,__const 等等,另外需要特別注意的是 Mach Header 和 Load Commands 的資訊本身也存在於 __TEXT 段中,__TEXT 段中資訊通常都是隻讀的,__DATA 是可讀寫的。

WWDC Mach-O 結構

點選 APP 啟動的過程

首先 iOS 系統也是程式碼編寫的,所以內部發生一系列的呼叫,感興趣的可以研究一下蘋果的核心原始碼,現在已經開源了。

核心建立程式程式 (fork),然後載入並執行程式(execve),接著發生一系列的呼叫,先是載入程式的 Mach-O(load_machfile)的執行,然後會有一個解析 Mach-O (parse_machfile)的呼叫,這一過程也伴隨著 mach header 的校驗,比如核心解析 mach file 的時候會去校驗當前程式是否被當前機器所支援,解析當前 Mach-O 的檔案型別是可執行檔案還是 dynamic link editor 等:

check machine type

接著 Load Commands 被對映到記憶體中,遍歷 Load Commands 中所有的 Load Command,然後根據對應的指令進行相應的操作,核心找到 LC_LOAD_DYLINKER 指令,然後返回對應的一個 ret:

case LC_LOAD_DYLINKER

LC_LOAD_DYLINKER 是隻有可執行檔案才有的指令,你可以用 MachOView 點開系統動態庫檢視,其實是沒有 LC_LOAD_DYLINKER 指令

可執行檔案 Mach-O LC_LOAD_DYLINKER 指令

如果條件滿足,就會執行載入動態連結器(the dynamic link editor 簡稱 dyld)的操作:

case LC_LOAD_DYLINKER

load_dylinker 做了一系列,可以用 file /usr/lib/dyld 檢視 dyld 也是 Mach-O 格式的一種,所以也有個 parse_machfile 的呼叫,其實和載入程式可執行檔案差不多,但是兩者型別並不同,用 MH_EXECUTE 和 MH_DYLINKER 區分,有興趣的可以看下核心原始碼中 load_dylinker 的方法具體實現。

當 dyld 被載入到記憶體中,最終 dyld __main 被執行:

case LC_LOAD_DYLINKER

核心做了一系列的準備以及 dyld 的載入,dyld 啟動後,接下來的事情將由 dyld 完成,dyld 在程式程式上執行和程式有著相同的許可權,接著就構成 WWDC 中介紹的 dyld 的工作流水線:

Load dylibs -> Rebase -> Bind -> ObjC -> Initializers

Load dylibs

接下來的步驟就比較好理解了,dyld 啟動後將會在可執行檔案的 Mach Header 上找到型別為 LC_LOAD_DYLIB 的載入指令,查詢需要的動態庫,因為每個 dylib 也是 Mach-O 映象檔案,需要執行同可執行檔案一樣類似對映到記憶體上,驗證 Mach header 等等,並且 dylib 會依賴其他的 dylib,所以 dylibs 載入是一個遞迴的過程。

如果需要檢視專案所有的動態庫載入詳細的資訊,可以開啟 Dynamic Library Loads,就會在執行時在控制檯顯示詳細的資訊:

設定連結動態庫資訊

一般 APP 通常會載入 100 到 400 個動態庫,基本上都是系統動態庫,不過系統動態庫載入是有被蘋果優化過的,所以耗時不是特別的明顯

Fix-ups

載入到記憶體的可執行檔案以及動態庫中的符號都是不可用的狀態,因為 ASLR 的存在,可執行檔案和動態連結庫在虛擬記憶體中載入的地址每次都是隨機的,所以需要 Fix-ups,Fix-ups 主要經過兩個步驟,即 Rebase -> Binding。

Rebase

Rebase:說白了就是因為 Mach-O 映象檔案載入到記憶體中的地址和初始地址不同,所以 Dyld 需要 rebase 去進行指標修正,點選 MachOView 的 Dynamic Loader info 檢視詳細的 rebase 資訊,rebase 主要是修復 __DATA 中的指標:

Rebase 資訊
Binding

因為動態庫不編譯程式序最終的二進位制檔案中,而是在執行的時候動態的查詢呼叫函式的地址,呼叫外部符號進行繫結的過程就稱作 Binding,比如專案中用到 UIView ,因為 UIView 屬於 UIKit 框架,所以需要進行繫結操作:

Binding 資訊

除了 Binding 還有 Lazy Binding、Weak Binding 感興趣的可以繼續深入去研究,由於篇幅問題暫不繼續介紹了,這裡主要介紹 WWDC 中的部分。

關於 Mach-O 各個 section 代表的具體意思,可以從這篇文章上獲取(找半天並沒有在蘋果官方上找到 Mach-O ABI,只找到了別人轉存的)

Objc Setup

資料修正後將會註冊 Objc 的類,如果有分類,還需要將分類定義的方法插入的方法列表中,並且保證每一個 selector 是唯一的

initializers

經過上述的 fix-ups 之後最後一步就是進行靜態初始化工作,例如 Load 函式,如果有 C++ 的一些初始化建構函式也將會被執行。

main 之後

執行完上述的步驟之後,接著 dyld 就會呼叫 main(),接著就是我們熟悉的步驟了:

UIApplicationMain() -> willFinishLaunchingWithOptions -> didFinishLaunchingWithOptions

所以如果 willFinishLaunchingWithOptions 和 didFinishLaunchingWithOptions 中存在耗時操作將會影響 APP main 之後的啟動時間。

美人相機優化實施

對流程每一步都很熟悉之後優化 APP 啟動就會得心應手,發現美人相機啟動瓶頸從而針對每一項做出改進。

動態庫載入方面

因為主要的動態庫就是系統的動態庫,又由於系統本身進行的一些優化處理,動態庫的載入對美人相機的影響其實不是很大,所以優化空間不是很大,但是由於如果連結進無用的系統動態庫,並且動態庫和動態庫之間存在依賴,動態庫過多也會造成一些 bind 的一些損耗。

因為美人相機專案迭代問題,工程還是之前較老的設定,為了竟可能的去避免,一些系統框架專案沒有用到,但是還是匯入了的問題,這裡採取的一個優化措施是刪除 Link Binary With Libraries 中的所有的系統動態庫,改成自動 Link 系統動態庫:

Link Automatically

程式碼部分優化

因為 rebase / binding 主要存在 I/O 瓶頸,所以減少 rebase / binding 就需要減少 __DATA 段中的指標數量,這個前面也介紹了,rebase / binding 主要針對的就是 __DATA 中的指標數量,指標數量越多,資料修復的時間就越長,使用 AppCode 分析美人相機程式碼:

Appcode 專案分析

TIPS:因為 AppCode 也會分析 Pods 內部的程式碼,試了設定了也沒用,都會去檢測,檢測時間非常非常非常的久,所以有個比較取巧的辦法就是,先移除 Pods 的程式碼,單純的分析美人相機工程的程式碼。

檢測完接下來就是單純的體力活,但是需要注意的是,AppCode 檢測的結果並非準確,而且一些大廠龐大的專案可能會忽略這一步,工作量比較大,而且因為 Runtime 的原因,可能存在一些執行時的對映,也會被誤檢到。

結合美人相機專案並且為了後續迭代版本考慮,長痛不如短痛,綜合考慮,這一步還是進行了實施,所以總的就是 AppCode 分析結果 + 人工 Check,移除了專案中無用的類,分類和無用的方法。

程式碼部分優化其他方面

如果大量使用 C++ 程式碼的部分,需要減少 C++ 虛擬函式的使用,因為虛擬函式建立的虛表也會存在 __DATA 段中,目前美人相機上層部分暫時沒有大量使用 C++ ,除了底層人臉識別的程式碼庫,這部分將會和公司底層開發同學溝通關於這部分規範的制定。

還有就是蘋果推薦的 Swift Struct 型別,因為需要 Fix-ups 的內容比較少。

關於 Objc setup 這部分的優化

Objc setup 這部分由於 Fix-ups 中 rebase / binding 部分優化過,所以這部分可以優化的基本上都已經被優化過了,因為基本上類數量和 selector 減少了,所以 setup time 也會相應的減少。

initializers 部分的優化

這部分在美人相機中,其實主要就是 +load 中程式碼可能會造成的影響

蘋果已經不推薦使用 +load,建議使用 +initialize 懶載入的方式,而不是隻要類被裝載在專案中就會執行 +load,如果 +load 中存在大量耗時操作,對啟動造成的影響非常大

關於 +load 可以採用 instrument 進行檢測分析(之前好像沒太注意),找到耗時的 load 操作,一般在 load 中做大量耗時操作本身就是一個不規範的操作了,使用 instrument 的方法就是,取 initializing 階段樣本進行分析 :

instrument initializing

根據分析結果找到耗時操作,進行相對應的優化,在 load 優化的時候,遇到一個問題,發現使用的 AF 的庫造成的影響非常的大:

AF time

檢視 AF +load 函式耗時的地方主要是生成 configuration 的時候:

但是對新建的一些專案還有 AF 的 demo 進行分析,發現其實 AF load 不是耗時那麼多,後面查詢資料並沒有找到相關的介紹,這是我一個比較疑惑的地方,一直沒能解決,不清楚的是這個屬不屬於正常現象,如果有踩過坑或者知道的大佬,希望可以解答一下我的疑惑。

update:後面再次檢測發現這個問題又恢復正常了,真是一個讓人百思不得其解的問題。。。

和 +load 類似的還有 atribute((constructor)) ,它將方法顯示的標記為初始化器,這樣只要程式執行都會去執行,如果存在耗時操作,將會對啟動造成影響,所以建議少用,美人相機檢測了一下,沒有發現,所以主要的還是 load 的影響。

因為使用 Xcode 除錯的話,會注入一些除錯的庫,所以也會算進去 pre-main 的時間:

注入除錯相關庫的載入耗時

如果不想受到這些動態庫連結的影響,可以在 scheme 中關閉:

關閉 GPU 除錯相關

關於 AppDlegate 中程式碼的優化

美人相機中主要是 didFinishLaunchingWithOptions ,排查 didFinishLaunchingWithOptions 中可能的耗時部分,使用 instrument 進行分析:

didFinishLaunchingWithOptions 耗時

其實對於 main 之後的影響,首頁介面的渲染並沒有出現在 didFinishLaunchingWithOptions 中,如果在 didFinishLaunchingWithOptions 使用到了 rootViewController.view 的話,那結果就不一樣了,目前美人相機的邏輯主要耗時部分存在 appSetting 處,進一步排查分析,發現耗時的竟然是美人相機接入的神策的統計的一處的程式碼:

神策統計耗時

奇怪的地方是,一般統計 SDK 並不會造成特別明顯的耗時操作,經過一系列的分析和查詢神策文件,發現其實是美人相機 pod 神策 SDK 的方式有問題,發現問題然後在內部進行溝通討論確定美人相機統計不需要上傳函式堆疊之後,更改了神策的 pod 方式,關閉堆疊解析上傳:

更改神策統計 POD 方式

更改後神策統計部分耗時由原來在 didFinishLaunchingWithOptions 佔比 15% 佔了較大部分減少到了 4.1% (可以和百度統計對比觀察):

優化前:

優化前

優化後:

優化後

關於第三方 SDK 的問題,已經和 iOS 小組的組長提出建議,接入 SDK 的時候要仔細研究文件,或者詢問第三方 SDK 的供應商,對第三方 SDK 的接入利弊需要做一個清晰的瞭解之後再接入

非啟動時間影響的首頁檢視優化

因為目前美人相機的程式碼邏輯,經過檢測,目前首先建立顯示的時間不會影響啟動時間,但是會影響使用者對首頁呈現是否流暢的感覺,所以針對首頁也進行了一些優化,其中包括,非第一次顯示的檢視採用延時載入,減少 CPU 的佔有率。

優化效果呈現

本次優化採用的機器是我一個比較舊的 iPhone5s iOS 11 系統,建議使用舊機器進行效能優化,統計的時間是關於 pre-main 之前熱啟動的時間(即已經啟動過而非第一次安裝或清除後臺資料重新啟動的資料),因為 pre-main 時間存在波動,所以進行了多次啟動資料的統計,這裡取了10個樣本,有個大致直觀的感受,並且 Debug 和 Release 不同,release 版本進行的 link 優化等操作,所以整體的時間還需要實際操作感受:

pre-main 優化效果

關於 main 之後的效果上面已經說到過了。

額外的點

目前系統使用的動態連結庫程式為 dyld2 ,蘋果也有介紹 dyld3,目前 iOS 11 所有的系統應用都是採用 dyld3 ,會有一部分的改變,並且啟動會得到優化,同個程式,用 dyld3 啟動的會比 dyld2 更快,未來所有的第三方應用也將會採用 dyld3 來進行載入,關於如何過渡到 dyld3 以及 dyld3 的詳細介紹可以觀看 WWDC 視訊進行了解。

總結

這篇針對 WWDC 視訊中的疑惑,自己去進行了一系列的探索和學習,並且主要針對核心啟動 APP 以及 dyld 如何被啟動的做了一次分析,以及 Mach-O 檔案的結構,對 rebase 和 binding 有個直觀的感受,可以比較清楚的對症下藥,關於 dyld 原始碼部分,以及具體詳細的過程,由於篇幅和時間問題,dyld 原始碼載入過程就沒有詳細展開,只針對 WWDC 介紹的幾個步驟進行了展開,有機會再輸出一篇關於 dyld 原始碼學習的記錄,如果文章存在部分錯誤的地方,還請多多指正。

參考連結

Apple 核心原始碼
Apple dyld 原始碼
優化 App 的啟動時間
Optimizing App Startup Time WWDC
iOS一次立竿見影的啟動時間優化 - 簡書

相關文章