前言
當使用者按下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 | |
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
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZlBna9QXbm9Fe39DMvw1dTJXbDN0c5VkV2FGN2Jme0UmUxYXOl1UNnJGenllalZXRzglWjNWaSlVU6lkRl5GZ3ZTN1c1bxVXSzZlYpFFOntkYkZVTvRXeO1UW5c2YpRVbyoUYphmUkZ2LcdGcq9lepJWbt9CXuNmLvd2bsFnL6lmYt12Lc9CX6MHc0RHaiojIsJye.jpg)
檢測耗時
可以看到,我的 APP 加載時間并沒有很慢,但是也想看一看有沒有優化的空間。
在 didFinishLaunchingWithOptions 方法裡我們一般都有以下的邏輯:
初始化第三方 SDK
配置 APP 運作需要的環境
自己的一些工具類的初始化
...
這裡主要參考[iOS]一次立竿見影的啟動時間優化
從優化圖可以看到,我的應用的跳轉邏輯是 打開 -> 廣告頁 -> 首頁,首頁的UI 架構是:
UITabBarC管理一堆 UINavigationC
但是如果 UI 架構如上,并且在didFinishLaunchingWithOptions裡面設定了根視圖
1 2 3 4 5 6 7 8 9 10 11 12 | |
然後我們來到 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 方法裡,這裡完全不會影響到啟動時間。
優化後
這是優化後的啟動時間
優化思路
梳理各個三方庫,找到可以延遲加載的庫,做延遲加載處理,比如放到首頁控制器的viewDidAppear方法裡。
梳理業務邏輯,把可以延遲執行的邏輯,做延遲執行處理。比如檢查新版本、注冊推送通知等邏輯。
避免複雜/多餘的計算。
避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個方法執行完,首頁控制器才能顯示,部分可以延遲建立的視圖應做延遲建立/懶加載處理。
采用性能更好的API。
首頁控制器用純代碼方式來建構。
另:[iOS]一次立竿見影的啟動時間優化 提到了使用一個工具類來管理的方法,可以比較友善的管理優化。
總結
成本效益最高的優化階段就是t2的一些邏輯整理,盡量将不需要的耗時操作延遲到首屏展示之後執行。
同時一般來說,優化應該在項目完成穩定之後進行,避免過早優化.