本文介紹App啟動性能優(yōu)化,共分五個部分:
第一部分App啟動過程
第二部分pre-main階段的過程和可優(yōu)化項
第三部分main()階段可優(yōu)化項
第四部分啟動耗時的測量
第五部分總結我們app需要做的啟動性能優(yōu)化
【第一部分】App啟動過程
iOS應用的啟動可分為pre-main階段和main()階段,其中系統(tǒng)做的事情依次是:

無論對于系統(tǒng)的動態(tài)鏈接庫還是對于App本身的可執(zhí)行文件而言,他們都算是image(鏡像),而每個App都是以image(?鏡像)為單位進行加載的
什么是image
1、Executable: 應用的主要二進制(比如.o文件)
2、Dylib: 動態(tài)鏈接庫(dynamic library,又稱 DSO 或 DLL)
3、Bundle: 資源文件,不能被鏈接的 Dylib,只能在運行時使用?dlopen()?加載
1. pre-main階段
1.1. 加載應用的可執(zhí)行文件(自身App的所有.o文件的集合)
1.2. 加載動態(tài)鏈接器dyld(dynamic loader,是一個專門用來加載動態(tài)鏈接庫的庫)
1.3. dyld遞歸加載應用所有依賴的動態(tài)鏈接庫dylib
2. main()階段
2.1. dyld調(diào)用main()?
2.2. 調(diào)用UIApplicationMain()?
2.3. 調(diào)用applicationWillFinishLaunching
2.4. 調(diào)用didFinishLaunchingWithOptions
【第二部分】pre-main階段的過程和可優(yōu)化項
要對pre-main階段的耗時做優(yōu)化,需要再學習下dyld加載的過程,根據(jù)Apple在2016WWDC上的介紹,dyld的加載主要分為4步:
1. Load dylibs

這一階段dyld會分析應用依賴的dylib(xcode7以后.dylib已改為名.tbd),找到其mach-o文件,打開和讀取這些文件并驗證其有效性,接著會找到代碼簽名注冊到內(nèi)核,最后對dylib的每一個segment調(diào)用mmap()。
一般情況下,iOS應用會加載100-400個dylibs,其中大部分是系統(tǒng)庫,這部分dylib的加載系統(tǒng)已經(jīng)做了優(yōu)化。
所以,依賴的dylib越少越好。在這一步,我們可以做的優(yōu)化有:
1.1、盡量不使用內(nèi)嵌(embedded)的dylib,加載內(nèi)嵌dylib性能開銷較大
1.2、合并已有的dylib和使用靜態(tài)庫(static archives),減少dylib的使用個數(shù)
1.3、懶加載dylib,但是要注意dlopen()可能造成一些問題,且實際上懶加載做的工作會更多
2. Rebase/Bind

在dylib的加載過程中,系統(tǒng)為了安全考慮,引入了ASLR(Address Space Layout Randomization)技術和代碼簽名。由于ASLR的存在,鏡像(Image,包括可執(zhí)行文件、dylib和bundle)會在隨機的地址上加載,和之前指針指向的地址(preferred_address)會有一個偏差(slide),dyld需要修正這個偏差,來指向正確的地址。
Rebase在前,Bind在后,Rebase做的是將鏡像讀入內(nèi)存,修正鏡像內(nèi)部的指針,性能消耗主要在IO。Bind做的是查詢符號表,設置指向鏡像外部的指針,性能消耗主要在CPU計算。
所以,指針數(shù)量越少越好。在這一步,我們可以做的優(yōu)化有:
2.1、減少ObjC類(class)、方法(selector)、分類(category)的數(shù)量
2.2、減少C++虛函數(shù)的的數(shù)量(創(chuàng)建虛函數(shù)表有開銷)
2.3、使用Swift structs(內(nèi)部做了優(yōu)化,符號數(shù)量更少)
3. Objc setup

大部分ObjC初始化工作已經(jīng)在Rebase/Bind階段做完了,這一步dyld會注冊所有聲明過的ObjC類,將分類插入到類的方法列表里,再檢查每個selector的唯一性。
在這一步倒沒什么優(yōu)化可做的,Rebase/Bind階段優(yōu)化好了,這一步的耗時也會減少。
4. Initializers

到了這一階段,dyld開始運行程序的初始化函數(shù),調(diào)用每個Objc類和分類的+load方法,調(diào)用C/C++ 中的構造器函數(shù)(用attribute((constructor))修飾的函數(shù)),和創(chuàng)建非基本類型的C++靜態(tài)全局變量(通常是類或結構體)。Initializers階段執(zhí)行完后,dyld開始調(diào)用main()函數(shù)。
Objc的load函數(shù)和C++的靜態(tài)構造函數(shù)采用由底向上的方式執(zhí)行,來保證每個執(zhí)行的方法,都可以找到所依賴的動態(tài)庫。例:

在這一步,我們可以做的優(yōu)化有:
4.1、少在類的+load方法里做事情,盡量把這些事情推遲到+initiailize
4.2、減少構造器函數(shù)個數(shù),在構造器函數(shù)里少做些事情
4.3、減少C++靜態(tài)全局變量的個數(shù)
【第三部分】main()階段的可優(yōu)化項
這一階段的優(yōu)化主要是減少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里,我們會創(chuàng)建應用的window,指定其rootViewController,調(diào)用window的makeKeyAndVisible方法讓其可見。由于業(yè)務需要,我們會初始化各個三方庫,設置系統(tǒng)UI風格,檢查是否需要顯示引導頁、是否需要登錄、是否有新版本等,由于歷史原因,這里的代碼容易變得比較龐大,啟動耗時難以控制。
所以,滿足業(yè)務需要的前提下,didFinishLaunchingWithOptions在主線程里做的事情越少越好。在這一步,我們可以做的優(yōu)化有:
1、梳理各個/三方庫,找到可以延遲加載的庫,做延遲加載處理,比如放到首頁控制器的viewDidAppear方法里。
2、梳理業(yè)務邏輯,把可以延遲執(zhí)行的邏輯,做延遲執(zhí)行處理。比如檢查新版本、注冊推送通知等邏輯。
3、避免復雜/多余的計算。
4、采用性能更好的API。
5、避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個方法執(zhí)行完,首頁控制器才能顯示,部分可以延遲創(chuàng)建的視圖應做延遲創(chuàng)建/懶加載處理。
6、首頁控制器用純代碼方式來構建。
【第四部分】啟動耗時的測量
在進行優(yōu)化之前,我們首先應該能測量各階段的耗時。
1. pre-main階段測量
對于pre-main階段,Xcode9之后,Apple提供了一種測量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環(huán)境變量DYLD_PRINT_STATISTICS 設為1 :

設置好后把程序跑起來,控制臺會有如下輸出,pre-main階段各過程的耗時一覽無余

如何解讀
1、pre-main階段(main()函數(shù)之前)總共使用了2.1s(5s,i0S11.1測試)
2、在2.1s中,加載動態(tài)庫用了1.2s,指針重定位使用了222.07ms,ObjC類初始化使用了174.56ms,各種初始化使用了521.02ms。
3、在初始化耗費的521.02ms中,用時最多的四個初始化是libSystem.B.dylib、libMainThreadChecker.dylib、libglInterpose.dilib以及teacher。
2. main()階段測量
對于main()階段,主要是測量main()函數(shù)開始執(zhí)行到didFinishLaunchingWithOptions執(zhí)行結束的耗時,就需要自己插入代碼到工程中了。先在main()函數(shù)里用變量StartTime記錄當前時間:

再在AppDelegate.h文件中用extern聲明全局變量StartTime

最后在didFinishLaunchingWithOptions里,再獲取一下當前時間,與StartTime的差值即是main()階段運行耗時。

【第五部分】總結我們app需要做的啟動性能優(yōu)化
1. pre-main階段的優(yōu)化
順便先說一下,?pre-main階段優(yōu)化到什么范圍內(nèi)比較好呢,蘋果給出的建議最好是400ms之內(nèi),但這個肯定要按照項目的實際情況有所取舍。

1.1、排查無用的dylib(不確定的可以先刪除,在編譯下項目試試),減少dylib的數(shù)目
1.2、檢查 framework應當設為optional和required,如果該framework在當前App支持的所有iOS系統(tǒng)版本都存在,那么就設為required,否則就設為optional
1.3、減少ObjC類(項目中不常用的庫,廢棄的代碼等)、方法(selector)、分類(category)的數(shù)量、無用的庫、非基本類型的C++靜態(tài)全局變量(通常是類或結構體)
1.4、壓縮資源圖片,刪除無用的圖片(IO操作)
1.4、少在類的+load方法里做事情,盡量把這些事情推遲到+initiailize
1.5、使用Swift structs(這是長期工作,可以考慮未來新頁面用swift寫)
2. main()階段的優(yōu)化
2.1、可使用instruments的Time Profiler先分析啟動時哪些地方比較耗時,是否可以做優(yōu)化
2.2、梳理各個三方庫,找到可以延遲加載的庫,做延遲加載處理,比如放到首頁控制器或tabBar控制器的viewDidAppear方法里,并且保證只執(zhí)行一次(按項目結構,放在合適的地方)
2.3、梳理業(yè)務邏輯,把可以延遲執(zhí)行的邏輯,做延遲執(zhí)行處理。比如檢查新版本、注冊推送通知等邏輯。
2.4、避免復雜/多余的計算
2.5、每次用NSLog方式打印會隱式的創(chuàng)建一個Calendar,因此需要刪減啟動時各業(yè)務方打的log
2.6、避免在用戶看到的第一個界面(首頁控制器或注冊登錄頁面)的viewDidLoad和viewWillAppear做太多事情,這2個方法執(zhí)行完,第一個頁面才能顯示,部分可以延遲創(chuàng)建的視圖應做延遲創(chuàng)建/懶加載處理
2.7、首頁控制器或注冊登錄頁面用純代碼方式來構建
2.8、我們項目中每次啟動會全量拉取AppServerConfig的配置,內(nèi)容太多,未來需要api配合拆分,等頁面使用的時候在拉取相應配置
2.9、持久化數(shù)據(jù)的讀取到內(nèi)存中的時間也可以評估一下