從用戶點擊App圖標開始到appDelegate didFinishLaunching
方法執行完成爲止。html
分爲兩個階段:
T1:pre-main
階段,即main()
函數以前,即操做系統加載App可執行文件到內存,而後執行一系列的加載&連接等工做,最後執行至App的main()
函數;
T2:main()
函數以後,即從main()
開始,到appDelegate
的didFinishLaunchingWithOptions
方法執行完畢前這段時間,主要是構建第一個界面,並完成渲染。ios
從用戶點擊App圖標開始到用戶能看到App主界面內容爲止這個過程,即T1+T2。git
思路: 在main()
函數以後的didFinishLaunchingWithOptions
方法裏執行了各類業務,有不少業務不是必定要在這裏執行,咱們能夠延遲加載,防止影響啓動時間。程序員
在didFinishLaunchingWithOptions
方法裏咱們通常作一下邏輯:github
main階段的優化大體有如下幾點:shell
xib
或者storyboard
來進行UI框架的搭建,尤爲是主UI框架好比TabBarController
這種,儘可能避免使用xib
或者storyboard
,由於它們也仍是要解析成代碼纔去渲染頁面,多了一些步驟;上面這些優化點,都是前人們總結出來的,在本身的項目實際優化的過程當中,仍是須要結合業務邏輯來處理。緩存
筆者在實際操做的過程當中,先是經過工具檢測出這個過程當中,找出全部方法的耗時時長,而後根據具體的業務邏輯去優化的。性能優化
其實這階段的優化很明顯,只要咱們找出耗時操做,而後對其進行相應的分析作處理,該延遲調用的延遲,該懶加載的懶加載,便能縮短啓動時間。bash
能夠經過instrument
的Time profile
工具來分析耗時。以下是我實踐的過程。
首先對Xcode
進行配置:
步驟一:網絡
步驟二:
步驟三: 對項目進行command + shit + k
清除操做,而後command + R
運行,最後進行instrument
工具的喚起,利用快捷鍵command + i
便可。
Time Profiler
,點擊
Choose
便可。
爲了可以更加直觀的觀察,咱們能夠進行下面的配置
而後點擊左上角的紅色圓圈即可進行耗時檢測,以下圖:
從上面可以很是直觀的看到每一個方法以及對應的耗時,從這裏,咱們便可以找到哪些是咱們所要優化的。
若是你在上一步的檢測過程當中,發現TabBarController
的viewDidLoad
耗時較長,那麼就要進行下面的檢測。
首頁的viewDidLoad
以及viewWillAppear
方法中儘可能去嘗試少作,晚作,或者採起異步的方式去作。
以下一段代碼:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"didFinishLaunchingWithOptions 開始執行");
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
ISTabBarController *tabBarVc = [[ISTabBarController alloc]init];
self.window.rootViewController = tabBarVc;
[self.window makeKeyAndVisible];
NSLog(@"didFinishLaunchingWithOptions 跑完了");
return YES;
}
複製代碼
而後來到ISTabBarController
的viewDidLoad
方法裏進行他的viewControllers
的設置,而後再進入到每一個viewController
的viewDidLoad
方法裏進行更多的初始化操做。
2020-02-17 11:17:17.024481+0800 InnotechShop[3776:1477241] didFinishLaunchingWithOptions 開始執行
2020-02-17 11:17:17.034835+0800 InnotechShop[3776:1477241] 開始加載 ISTabBarController 的 viewDidLoad
2020-02-17 11:17:17.034934+0800 InnotechShop[3776:1477241] didFinishLaunchingWithOptions 跑完了
2020-02-17 11:17:17.034965+0800 InnotechShop[3776:1477241] 開始加載 ISViewController 的 viewDidLoad, 而後執行一堆初始化的操做
複製代碼
這種狀況是能保證咱們不在ISTabBarController
中操做ISViewController
的view
, 若是咱們在ISTabBarController
中操做ISViewController
的view
的話,那麼調用順序將會是下面這樣:
2020-02-17 11:23:42.018824+0800 InnotechShop[3796:1480231] didFinishLaunchingWithOptions 開始執行
2020-02-17 11:23:42.018883+0800 InnotechShop[3796:1480231] 開始加載 ISTabBarController 的 viewDidLoad
2020-02-17 11:23:42.018957+0800 InnotechShop[3796:1480231] 開始加載 ISViewController 的 viewDidLoad, 而後執行一堆初始化的操做
2020-02-17 11:23:42.019020+0800 InnotechShop[3796:1480231] didFinishLaunchingWithOptions 跑完了
複製代碼
這樣的話,咱們就把界面的初始化、網絡請求、數據解析、視圖渲染等操做都放在了viewDidLoad
方法裏,那麼每次啓動App的時候,在用戶看到第一個頁面以前,咱們都要把這些事情所有處理完成,纔會進入到視圖渲染階段。
因爲筆者項目的業務邏輯並非那麼的複雜,因此在實踐中大概作了一下幾點:
didFinishLaunchingWithOptions
進行初始化的操做,延遲到首頁渲染完成之後調用tabBarController
的viewDidLoad
函數裏,去掉一些沒必要要的函數調用;優化先後耗時對比:
對於didFinishLaunchingWithOptions
,這裏面的初始化是必須執行的,可是咱們能夠適當的根據功能的不一樣對應的適當延遲啓動的時機。對於咱們項目,我將初始化分爲三個類型:
對於第一類,因爲這類事件的特殊性,因此必須第一時間啓動,仍然把它留在didFinishLaunchingWithOptions
裏啓動。第二類事件,這些功能在用戶進入APP主體的以前是必需要加載完的,因此咱們能夠把它放在第二批,也就是用戶已經看到廣告頁面,再進行廣告倒計時的時候再啓動。第三類事件,因爲不是必須的,因此咱們能夠放在第一個界面渲染完成之後的viewDidAppear
方法裏,這裏徹底不會影響到啓動時間。
如今許多App在啓動時並不直接進入首頁,而是會向用戶展現一個持續一小段時間的閃屏頁,若是使用恰當,這個閃屏頁就能幫咱們節省一些啓動時間。 下面看兩組閃屏的流程對比便可發現好處:
未優化的閃屏流程:
優化的閃屏流程:
如下爲iPhone 7p正常啓動消耗的pre-main
時間(蘋果提供了內建的測量方法,在 Xcode 中 Edit scheme -> Run -> Auguments -> Environment Variables
點擊+
添加環境變量 DYLD_PRINT_STATISTICS
設爲 1
):
Total pre-main time: 608.72 milliseconds (100.0%)
dylib loading time: 308.40 milliseconds (50.6%)
rebase/binding time: 28.92 milliseconds (4.7%)
ObjC setup time: 22.50 milliseconds (3.6%)
initializer time: 248.89 milliseconds (40.8%)
slowest intializers :
libSystem.B.dylib : 3.75 milliseconds (0.6%)
libMainThreadChecker.dylib : 31.74 milliseconds (5.2%)
libglInterpose.dylib : 135.63 milliseconds (22.2%)
HelpDeskLite : 21.11 milliseconds (3.4%)
InnoAVKit : 20.81 milliseconds (3.4%)
InnotechShop : 29.62 milliseconds (4.8%)
複製代碼
解讀:
一、main()
函數以前總共用時608.72ms
二、在608.72ms中,加載動態庫使用了308.4ms,指針重定位用了28.92ms,ObjC類初始化使用了22.50ms,各類初始化使用了248.89ms
三、在初始化用時的248.89ms中,用時較多的幾個初始化是libglInterpose.dylib、ibMainThreadChecker.dylib、InnotechShop、InnoAVKit等
main()函數以前,基本上全部的工做都是系統完成的,開發者可以處理的地方很少,因此想要對這部分進行優化,那麼就須要瞭解一下這一過程系統都作了哪些事情,(原理部分的內容基本上都是網上摘錄的)。 這部分比較晦澀難懂,須要細品
pre-main
如圖,當啓動一個應用程序時,系統最後會根據你的行爲調用兩個函數,fork
和execve
。fork
功能建立一個進程;execve
功能加載和運行程序。這裏有多個不一樣的功能,好比execl,execv和exect
,每一個功能提供了不一樣傳參和環境變量的方法到程序中。在OSX中,每一個這些其餘的exec
路徑最終調用了內核路徑execve
。
一、執行
exec
系統調用,通常都是這樣,用fork()
函數新創建一個進程,而後讓進程去執行exec
調用。咱們知道,在fork()
創建新進程以後,父進程與子進程共享代碼段,但數據空間是分開的,但父進程會把本身數據空間的內容copy
到子進程中去,還有上下文也會copy
到子進程中去。
二、爲了提升效率,採用一種寫時copy
的策略,即建立子進程的時候,並不copy
父進程的地址空間,父子進程擁有共同的地址空間,只有當子進程須要寫入數據時(如向緩衝區寫入數據),這時候會複製地址空間,複製緩衝區到子進程中去。從而父子進程擁有獨立的地址空間。而對於fork()
以後執行exec
後,這種策略可以很好的提升效率,若是一開始就copy
,那麼exec
以後,子進程的數據會被放棄,被新的進程所代替
動態連接庫的加載過程主要由dyld來完成,dyld是蘋果的動態連接器 系統先讀取App的可執行文件(Mach-O文件),從裏面得到dyld的路徑,而後加載dyld,dyld去初始化運行環境,開啓緩存策略,加載程序相關依賴庫(其中也包含咱們的可執行文件),並對這些庫進行連接,最後調用每一個依賴庫的初始化方法,在這一步,runtime
被初始化。當全部依賴庫的初始化後,輪到最後一位(程序可執行文件)進行初始化,在這時runtime
會對項目中全部類進行類結構初始化,而後調用全部的load
方法。最後dyld返回main
函數地址,main
函數被調用,咱們便來到了熟悉的程序入口。
當你構建一個真正的程序時,將會連接各類各樣的庫。它們又會依賴其餘一些framework
和動態庫。須要加載的動態庫會很是多。而對於相互依賴的符號就更多了。可能將會有上千個符號須要解析處理,這將花費很長的時間 爲了縮短這個處理過程所花費時間,OS X 和 iOS 上的動態連接器使用了共享緩存,OS X的共享緩存位於/private/var/db/dyld/
,iOS的則在/System/Library/Caches/com.apple.dyle/
。 對於每一種架構,操做系統都有一個單獨的文件,文件中包含了絕大多數的動態庫,這些庫都已經連接爲一個文件,而且已經處理好了它們之間的符號關係。當加載一個 Mach-O 文件 (一個可執行文件或者一個庫) 時,動態連接器首先會檢查共享緩存看看是否存在其中,若是存在,那麼就直接從共享緩存中拿出來使用。每個進程都把這個共享緩存映射到了本身的地址空間中。這個方法大大優化了 OS X 和 iOS 上程序的啓動時間。
dyld的加載過程主要分爲下面幾個步驟:
在每一個動態庫的加載過程當中,dyld須要作下面工做:
- 分析因此來的動態庫
- 找到動態庫的mach-o文件
- 打開文件
- 驗證文件
- 在系統核心註冊文件簽名
- 對動態庫的每個segment調用mmap()
針對這一步的優化:
- 減小非系統庫的依賴
- 合併不是系統庫
看下筆者項目依賴的共享動態庫
輸入命令:otool -L XXXX
因爲ASLR(address space layout randomization)
的存在,可執行文件和動態連接庫在虛擬內存中的加載地址每次啓動都不固定,因此須要這2步來修復鏡像中的資源指針,來指向正確的地址。 rebase修復的是指向當前鏡像內部的資源指針; 而bind指向的是鏡像外部的資源指針。
rebase步驟先進行,須要把鏡像讀入內存,並以page爲單位進行加密驗證,保證不會被篡改,因此這一步的瓶頸在IO。bind在其後進行,因爲要查詢符號表,來指向跨鏡像的資源,加上在rebase階段,鏡像已被讀入和加密驗證,因此這一步的瓶頸在於CPU計算。
優化該階段的關鍵在於減小__DATA segment中的指針數量。咱們能夠優化的點有:
- 減小Objc類數量, 減小selector數量
- 減小C++虛函數數量
Objc setup
主要是在objc_init
完成的,objc_init
是在libsystem
中的一個initialize
方法libsystem_initializer
中初始化了libdispatch
,而後libdispatch_init
調用了_os_object_int
, 最終調用了_objc_init
。
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_2_images, load_images, unmap_image);
}
複製代碼
經過上面代碼能夠知道,runtime
在_objc_init
向dyld
綁定了3個回調函數,分別是map_2_images,load_images和unmap_image
一、dyld在
binding
操做結束以後,會發出dyld_image_state_bound
通知,而後與之綁定的回調函數map_2_images
就會被調用,它主要作如下幾件事來完成Objc Setup
:
- 讀取二進制文件的 DATA 段內容,找到與 objc 相關的信息
- 註冊 Objc 類
- 確保 selector 的惟一性
- 讀取 protocol 以及 category 的信息
二、
load_images
函數做用就是調用Objc的load
方法,它監聽dyld_image_state_dependents_initialize
通知
三、unmap_image
能夠理解爲map_2_images
的逆向操做
因爲以前2步驟的優化,這一步實際上沒有什麼可作的。幾乎都靠 Rebasing 和 Binding 步驟中減小所需 fix-up 內容。由於前面的工做也會使得這步耗時減小。
以上三步屬於靜態調整,都是在修改__DATA segment
中的內容,而這裏則開始動態調整,開始在堆和棧中寫入內容。 工做主要有:
一、 Objc的
+load()
函數
二、 C++的構造函數屬性函數 形如attribute((constructor)) void DoSomeInitializationWork()
三、 非基本類型的C++靜態全局變量的建立(一般是類或結構體)(non-trivial initializer) 好比一個全局靜態結構體的構建,若是在構造函數中有繁重的工做,那麼會拖慢啓動速度
Objc的load
函數和C++的靜態構造器採用由底向上的方式執行,來保證每一個執行的方法,均可以找到所依賴的動態庫
一、 dyld開始將程序二進制文件初始化
二、 交由ImageLoader
讀取image
,其中包含了咱們的類、方法等各類符號
三、 因爲runtime
向dyld綁定了回調,當image
加載到內存後,dyld會通知runtime
進行處理
四、runtime
接手後調用map images
作解析和處理,接下來load images
中調用callloadmethods
方法,遍歷全部加載進來的Class
,按繼承層級依次調用Class
的+load
方法和其Category
的+load
方法
整個事件由dyld主導,完成運行環境的初始化後,配合ImageLoader
將二進制文件按格式加載到內存,動態連接依賴庫,並由runtime
負責加載成objc 定義的結構,全部初始化工做結束後,dyld調用真正的main
函數
這一步可作的優化有:
+initialize
來代替+load
atribute((constructor))
將方法顯式標記爲初始化器,而是讓初始化方法調用時才執行。好比使用 dispatch_once()、pthread_once() 或 std::once()
。也就是在第一次使用時才初始化,推遲了一部分工做耗時。也儘可能不要用到C++的靜態對象。+load()
方法,用於在App啓動執行一些操做,+load()
方法在Initializers
階段被執行,但過多的+load()
方法則會拖慢啓動速度。 分析+load()
方法,看是否能夠延遲到App冷啓動後的某個時間節點。
筆者在處理這個問題的過程當中遇到一個坑,項目裏有防crash的類,裏面有大量的系統類的load
方法,針對系統的load
方法,咱們不用去優化,由於在啓動的過程當中,有可能initialize
方法也會被調用,並起不到優化的做用,反而仍是出現各類各樣的問題;另一點須要注意的問題是initialize
的重複調用問題,能用dispatch_once()
來完成的,就儘可能不要用到load
方法
統計了各個庫所佔的size(安裝包size優化的腳本),基本上一個公共庫越大,類越多,啓動時在pre-main
階段所需的時間也越多。 統計結果以下:
pod有源碼的庫(靜態庫):
第三方framework(其實也是靜態庫,只是腳本分開統計):
筆者項目中使用cocoapods並無設置use_frameworks
,因此pod管理的有源碼的第三方庫都是靜態庫的形式,而framework
形式的靜態庫基本都是第三方公司提供的服務
這個過程並無作任何優化,對庫進行了逐一排查,均爲正在使用,顧這個環節沒有優化,只是記錄了一下。
因爲Category
的實現原理,和ObjC的動態綁定有很強的關係,因此實際上類的擴展是比較佔用啓動時間的。儘可能合併一些擴展,會對啓動有必定的優化做用。不過我的認爲也不能由於它佔用啓動時間而去逃避使用擴展,畢竟程序員的時間比CPU的時間值錢,這裏只是強調要合併一些在工程、架構上沒有太大意義的擴展。
壓縮圖片爲何能加快啓動速度呢?由於啓動的時候大大小小的圖片加載個十來二十個是很正常的,圖片小了,IO操做量就小了,啓動固然就會快了。
以上內容就是本次在作啓動時間優化所涉及的內容,理論知識都是從網上查詢得知,具體實踐,筆者都一一嘗試,做爲記錄。
參考資料:
美團外賣iOS App冷啓動治理
iOS啓動優化-凌雲的博客
iOS啓動時間優化-第七章
iOS啓動時間優化-PerTerbin
iOS App 啓動性能優化