iOS 啟動時間優化


在 WWDC 2016 上首次提到了關於 App 應用啟動速度優化的話題:Session 406 Optimizing App Startup Time

一、冷啟動與熱啟動

熱啟動是,APP會恢復之前的狀態繼續運行,這種就是熱啟動,我們平時所說的APP在后台的存活時間,其實就是APP能執行熱啟動的最大時間間隔。而冷啟動則是APP從被加載到內存到運行的狀態,下面我們要講的主要是冷啟動。

  • 熱啟動:由於某種原因,APP 的狀態由 running 切換為 suspend,但此時 APP 並沒有被系統 kill掉,當我們再次把 APP 切換到前台的時候,此時啟動 app 所需要的數據仍然在緩存中,再次啟動的時候稱為熱啟動。通常情況下熱啟動能幫助提升啟動速度,但有時也可能會出現 app 卡死手動退出進程后重新打開仍然是卡死狀態。
  • 冷啟動:如果是比較長時間沒有啟動過 app 或者設備剛剛重啟,這種情況下啟動 app,就被稱為冷啟動。

二、查看啟動時間

  • 最佳速度:400ms。因為不添加任何同步任務從圖標被點擊到顯示 Launch Screen,然后 Launch Screen 消失這段時間就是 400ms。如果 app 啟動時間接近這個數值,那證明 app 的啟動任務已經優化到最佳。
  • 最慢速度:不可以大於 20s,否則會被系統殺掉。

配置 Xcode 環境變量在日志中打印啟動時間:

  • 打開工程 -> Edit Scheme -> Run -> Environment Variables

根據需要添加 DYLD_PRINT_STATISTICSDYLD_PRINT_STATISTICS_DETAILS 環境變量。1 表示 YES,開啟這個功能。


Total pre-main time: 617.58 milliseconds (100.0%)
         dylib loading time: 472.75 milliseconds (76.5%)
        rebase/binding time:  27.01 milliseconds (4.3%)
            ObjC setup time:  28.90 milliseconds (4.6%)
           initializer time:  88.76 milliseconds (14.3%)
           slowest intializers :
             libSystem.B.dylib :   8.81 milliseconds (1.4%)
    libMainThreadChecker.dylib :  14.42 milliseconds (2.3%)
                  AFNetworking :  18.43 milliseconds (2.9%)
                         Realm :  20.98 milliseconds (3.3%)
                     CYKJBasic :  12.96 milliseconds (2.0%)

包括執行以下步驟的所有時間

  1. 解析鏡像
  2. 映射鏡像
  3. Rebase 鏡像
  4. Bind 鏡像
  5. 鏡像初始化
  6. 調用 main()方法
  7. 調用 UIApplicationMain() 方法
  8. 調用 applicationWillFinishLaunching 回調

DYLD_PRINT_STATISTICS 環境變量只能幫助測量 Dyld 的啟動時間,無法幫助我們完整地測量 main() 方法執行之前的所有時間。

為了能夠解析 dylib 的符號表,debugger 需要在加載每個 dylib 的時候暫停一下,同時通過 USB 線來傳輸這些數據,這是非常耗時的,但是 DYLD_PRINT_STATISTICS 在測量的時候已經減去了這些耗時,因此不用擔心在 debug 模式下數據的准確性,而且測量出的耗時通常會比肉眼計算出來的耗時要小。

三、優化啟動

啟動時間其實包括了兩部分:main 函數之前和 main 函數到第一個界面的 viewDidAppear:。

所以,優化也是從兩個方面進行的,優化效果主要來自於后者,因為絕大多數 App 的瓶頸在自己的代碼里。而對於 pre-main 的優化能做的無非是減少不必要的動態庫引用、多個庫合並成一個,從上面的打印數據也可以看出,主要耗時是在 dylib loading,消耗 78.8% 的時間。

四、Main 函數之后

從 main 函數開始執行,到第一個界面顯示,期間一般做以下任務:

  1. 執行 AppDelegate 的代理方法,主要是 didFinishLaunchingWithOptions
  2. 初始化 Window,初始化基礎的 ViewController 結構
  3. 獲取數據(Local DB/Network),展示給用戶。

優化:

  1. 使用代碼繪制 UI,減少或者不用 xib 和 storyboard

  2. 延遲初始化和加載不必要的 UIViewController 和 View。

    比如 UITabBarController 有四個 Item,在啟動的時候盡量只初始化首頁的頁面,其它 Item 頁面先用空 VC 占位。而且首頁的內容中不必要的內容也可以先不初始化,做成懶加載形式,在用戶確實需要查看和使用時再初始化。

  3. 對於確實需要啟動時使用但又比較耗時的事物放倒后台處理,如果涉及到 UI 則在處理完成后把刷新任務放回主線程。

    • 日志功能,日志往往涉及到 DB 操作;
    • 文件讀取,比如讀取本地存儲的省份城市區縣文件和圖片處理;
    • 大量的計算,比如圖片處理,比較大的 json 數據轉 Model;
  4. 能延遲初始化的盡量延遲初始化

    三方 SDK 初始化,比如 Crash 統計,分享之類的,可以等到第一次調用再去初始化。

五、Main函數之前

Main 函數之前是 iOS 系統的工作,所以這部分的優化往往更具有通用性。

Pre-Main 包含以下工作:

- dylib loading time: 341.79 milliseconds (78.8%)
- rebase/binding time:  14.18 milliseconds (3.2%)
- ObjC setup time:  35.27 milliseconds (8.1%)
- initializer time:  41.79 milliseconds (9.6%)
- slowest intializers :
- libSystem.B.dylib :   3.40 milliseconds (0.7%)
- libMainThreadChecker.dylib :  19.68 milliseconds (4.5%)
- libViewDebuggerSupport.dylib :   8.75 milliseconds (2.0%)

Session 所要傳達的內容:

  • 使用 DYLD_PRINT_STATISTICS 測試啟動加載時間
  • 減少自定義的動態庫集成
  • 精簡原有的 Objective-C 類和代碼
  • 移除靜態的初始化操作
  • 使用更多的 Swift 代碼

優化:

  1. loading dylib

    啟動的第一步是加載動態庫,加載系統的動態庫使很快的,因為可以緩存,而加載內嵌的動態庫速度較慢。所以,提高這一步的效率的關鍵是:減少動態庫的數量

    加載 App 內嵌的動態庫比較耗時,因為每加載一個動態庫,系統都需要文件驗證、注冊簽名、針對 segment 進行 mmap。embedded framework 一般用於 Extension 跟主 APP 共享代碼邏輯。

    合並動態庫,比如公司內部由私有 Pod 建立了如下動態庫:XXTableView、XXHUD、XXLabel,強烈建議合並成一個 XXUIKit 來提高加載速度。盡量保證將 App 現有的非系統級的動態庫個數保證在 6 個以內.

    也可以使用靜態庫,雖然會增加些許 rebase/binding 的時間。

  2. rebase/binding

    Rebase 和 Binding 都是為了解決指針引用的問題。所以為了加快 rebase/binding,則需要更少的做指針修復。當你的 app 當中有太多的 Objective-C 的類、方法選擇器和類別,會增加這一部分的啟動時間。有一個數據:當大於 20000 個時候,會增加 800ms 的時間。

    對於 Objective-C 開發來說,主要的時間消耗在 Class/Method 的符號加載上,所以常見的優化方案是:

    • 減少 __DATA 段中的指針數量。
    • 合並 Category 和功能類似的類,減少唯一 Selector 的個數。主要是為了加快程序的整個動態鏈接,在進行動態庫的重定位和綁定(Rebase/binding)過程中減少指針修正的使用,加快程序機器碼的生成。
    • 刪除無用的方法和類。
    • 多用 Swift Structs,因為 Swfit Structs 是靜態分發的。可以參考Swift進階之內存模型和方法調度
    • 減少 c++ 虛函數
  3. ObjC setup time

    在 Objective-C 的運行時(runtime),需要對類(class),類別(category)進行注冊,以及選擇器的分配,所以參照 rebase/binding time,盡量減少類的數量,可以達到減少這一部分的時間。

  4. Initializers

    • 用 initialize 替代 load。load 在程序啟動的時候就會調用,而且必須阻塞等着所有類的 load 方法都執行完;initialize 在類首次使用的時候調用。
    • 減少使用 c/c++ 的 __atribute__((constructor))。推薦使用 dispatch_once()、pthread_once()、std:once()等方法;
    • 推薦使用 swift
    • 不要在初始化中調用 dlopen() 方法,因為加載過程是單線程,無鎖,如果調用 dlopen 則會變成多線程,會開啟鎖的消耗,同時有可能死鎖
    • 不要在初始化中創建線程

文章

楓林風雨 - iOS性能(二) 啟動時間優化


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM