天天看點

【ios學習】優化 App 的啟動時間實踐 iOS

前言

當使用者按下home鍵的時候,iOS的App并不會馬上被kill掉,還會繼續存活若幹時間。理想情況下,使用者點選App的圖示再次回來的時候,App幾乎不需要做什麼,就可以還原到退出前的狀态,繼續為使用者服務。這種持續存活的情況下啟動App,我們稱為熱啟動,相對而言冷啟動就是App被kill掉以後一切從頭開始啟動的過程。我們這裡隻讨論App冷啟動的情況。

對于冷啟動來說,啟動時間是指從使用者點選 APP 那一刻開始到使用者看到第一個界面這中間的時間。我們進行優化的時候,我們将啟動時間分為 pre-main 時間和 main 函數到第一個界面渲染完成時間這兩個部分。

因為 APP 的入口在 main 函數 ,在 main 函數之後我們的代碼才會執行。

這裡有兩個階段

1. pre-main階段

1.1. 加載應用的可執行檔案

1.2. 加載動态連結庫加載器dyld(dynamic loader)

1.3. dyld遞歸加載應用所有依賴的dylib(dynamic library 動态連結庫)

2. main()階段

2.1. dyld調用main() 

2.2. 調用UIApplicationMain() 

2.3. 調用applicationWillFinishLaunching

2.4. 調用didFinishLaunchingWithOptions

我們把 pre-main階段稱為 t1,main()階段一直到首個頁面加載完成稱為 t2。

t1 時間的優化分析

t1部分主要參考自APP啟動優化的一次實踐

其中 t1蘋果提供了内建的測量方法, Xcode 中 Edit scheme -> Run -> Auguments 将環境變量 DYLD_PRINT_STATISTICS 設為 1

1 2 3 4 5 6 7 8 9

//結果為

Total pre-main time: 

1.4

seconds (

100.0

%)

dylib loading time: 

1.3

seconds (

89.4

%)

rebase/binding time:  

36.75

milliseconds (

2.5

%)

ObjC setup time:  

35.65

milliseconds (

2.4

%)

initializer time:  

80.97

milliseconds (

5.5

%)

slowest intializers :

libSystem.B.dylib :  

12.63

milliseconds (

0.8

%)

//解讀

1、main()函數之前總共使用了1.4s

2、在94.33ms中,加載動态庫用了1.3s,指針重定位使用了36.75ms,ObjC類初始化使用了35.65ms,各種初始化使用了80.97ms。

3、在初始化耗費的80.97ms中,用時最多的初始化是libSystem.B.dylib。

可以看到,我的 dylib loading time 花費了 1.3s時間,

其中各部分的作用是

加載dylib

分析每個dylib(大部分是iOS系統的),找到其Mach-O檔案,

打開并讀取驗證有效性,找到代碼簽名注冊到核心,

最後對dylib的每個segment調用mmap()。

rebase/bind

dylib加載完成之後,它們處于互相獨立的狀态,需要綁定起來。

在dylib的加載過程中,系統為了安全考慮,引入了ASLR(Address Space Layout Randomization)技術和代碼簽名。

由于ASLR的存在,鏡像(Image,包括可執行檔案、dylib和bundle)會在随機的位址上加載,和之前指針指向的位址(preferred_address)會有一個偏差(slide),dyld需要修正這個偏差,來指向正确的位址。

Rebase在前,Bind在後,Rebase做的是将鏡像讀入記憶體,修正鏡像内部的指針,性能消耗主要在IO。

Bind做的是查詢符号表,設定指向鏡像外部的指針,性能消耗主要在CPU計算。

OC setup

OC的runtime需要維護一張類名與類的方法清單的全局表。

dyld做了如下操作:

對所有聲明過的OC類,将其注冊到這個全局表中(class registration)

将category的方法插入到類的方法清單中(category registration)

檢查每個selector的唯一性(selector uniquing)

如果在各個 OC 類别的 ‘load’方法裡做了不少事情(如在裡面使用 Method swizzle),那麼這是pre-main階段最耗時的部分。dyld運作APP的初始化函數,調用每個OC類的+load方法,調用C++的構造器函數(attribute((constructor))修飾),建立非基本類型的C++靜态全局變量,然後執行main函數。

優化思路是

1. 移除不需要用到的動态庫

2. 移除不需要用到的類

3. 合并功能類似的類和擴充

4. 盡量避免在+load方法裡執行的操作,可以推遲到+initialize方法中。

t2 時間的優化分析

t2使用了來自NewPan大大 的打點計時器BLStopwatch

【ios學習】優化 App 的啟動時間實踐 iOS

檢測耗時

可以看到,我的 APP 加載時間并沒有很慢,但是也想看一看有沒有優化的空間。

在 didFinishLaunchingWithOptions 方法裡我們一般都有以下的邏輯:

初始化第三方 SDK

配置 APP 運作需要的環境

自己的一些工具類的初始化

...

這裡主要參考[iOS]一次立竿見影的啟動時間優化

從優化圖可以看到,我的應用的跳轉邏輯是 打開 -> 廣告頁 -> 首頁,首頁的UI 架構是:

【ios學習】優化 App 的啟動時間實踐 iOS

UITabBarC管理一堆 UINavigationC

但是如果 UI 架構如上,并且在didFinishLaunchingWithOptions裡面設定了根視圖

1 2 3 4 5 6 7 8 9 10 11 12

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

NSLog(@

"didFinishLaunchingWithOptions 開始執行"

);

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

TestTabBarController *tabBarVc = [TestTabBarController 

new

];

self.window.rootViewController = tabBarVc;

[self.window makeKeyAndVisible];

NSLog(@

"didFinishLaunchingWithOptions 跑完了"

);

return

YES;

}

然後我們來到 TestTabBarController 裡的 viewDidLoad方法裡進行它的 viewControllers 的設定,然後再進入到每個 viewController 的 viewDidLoad 方法裡進行更多的初始化操作。那麼你覺得從 didFinishLaunchingWithOptions 到最後顯示展示的 viewController 的 viewDidLoad 這些方法的執行順序是怎麼樣的呢?

didFinishLaunchingWithOptions 開始執行 

開始加載 TestTabBarController 的 viewDidLoad

didFinishLaunchingWithOptions 跑完了

開始加載 TestViewController 的 viewDidLoad, 然後執行一堆初始化的操作

在TestTabBarController 中操作了 TestViewController 的 view 的話,那麼調用順序将會是這樣:

didFinishLaunchingWithOptions 開始執行 

開始加載 TestTabBarController 的 viewDidLoad

開始加載 TestViewController 的 viewDidLoad, 然後執行一堆初始化的操作

didFinishLaunchingWithOptions 跑完了

這樣的問題就是當我們把界面的初始化、網絡請求、資料解析、視圖渲染等操作放在了viewDidLoad 方法裡,這樣一來每次啟動 APP 的時候,在使用者看到第一個頁面之前,我們要把這些事件全部都處理完,才會進入到視圖渲染階段。

一般來說,我們放到didFinishLaunchingWithOptions執行的代碼,有很多初始化操作,如日志,統計,SDK配置等。盡量做到隻放必需的,其他的可以延遲到MainViewController展示完成viewDidAppear以後。

* 日志、統計等必須在 APP 一啟動就最先配置的事件

* 項目配置、環境配置、使用者資訊的初始化 、推送、IM等事件

* 其他 SDK 和配置事件

  • 第一類,必須第一時間啟動,仍然把它留在 didFinishLaunchingWithOptions 裡啟動。
  • 第二類,這些功能在使用者進入 APP 主體的之前是必須要加載完的,我把他放到廣告頁面的viewDidAppear啟動。
  • 第三類,由于啟動時間不是必須的,是以我們可以放在第一個界面的 viewDidAppear 方法裡,這裡完全不會影響到啟動時間。
【ios學習】優化 App 的啟動時間實踐 iOS

優化後

這是優化後的啟動時間

優化思路

梳理各個三方庫,找到可以延遲加載的庫,做延遲加載處理,比如放到首頁控制器的viewDidAppear方法裡。

梳理業務邏輯,把可以延遲執行的邏輯,做延遲執行處理。比如檢查新版本、注冊推送通知等邏輯。

避免複雜/多餘的計算。

避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個方法執行完,首頁控制器才能顯示,部分可以延遲建立的視圖應做延遲建立/懶加載處理。

采用性能更好的API。

首頁控制器用純代碼方式來建構。

另:[iOS]一次立竿見影的啟動時間優化 提到了使用一個工具類來管理的方法,可以比較友善的管理優化。

總結

成本效益最高的優化階段就是t2的一些邏輯整理,盡量将不需要的耗時操作延遲到首屏展示之後執行。

同時一般來說,優化應該在項目完成穩定之後進行,避免過早優化.