深入探索 iOS 啟動速度優化

来源:https://www.cnblogs.com/simplepp/archive/2020/05/13/12882533.html
-Advertisement-
Play Games

介紹 App 的啟動時間是體現其性能優劣的一個重要指標,啟動時間越快用戶的等待時間就越短,提升用戶體驗感,大廠應用甚至會做到“ 毫秒必究 ”。 我們將 App 啟動方式分為: 名稱說明 冷啟動 App 啟動時,應用進程不在系統中(初次打開或程式被殺死),需要系統分配新的進程來啟動應用。 熱啟動 Ap ...


 

 

介紹

App 的啟動時間是體現其性能優劣的一個重要指標,啟動時間越快用戶的等待時間就越短,提升用戶體驗感,大廠應用甚至會做到“ 毫秒必究 ”。

我們將 App 啟動方式分為:

名稱說明
冷啟動 App 啟動時,應用進程不在系統中(初次打開或程式被殺死),需要系統分配新的進程來啟動應用。
熱啟動 App 退回後臺後,對應的進程還在系統中,啟動則將應用返回前臺展示。

本篇文章主要針對冷啟動方式進行優化分析,介紹常用的檢測工具及優化方法。

冷啟動流程

Apple 官方的《WWDC Optimizing App Startup Time》 將 iOS 應用的啟動可分為 pre-main 階段和 main 兩個階段,最佳的啟動速度是400ms以內,最慢不得大於20s,否則會被系統進程殺死(最低配置設備)。

為了更好的區分,筆者將整個啟動流程分為三個階段, App總啟動流程 = pre-main + main函數代理(didFinishLaunchingWithOptions)+ 首屏渲染(viewDidAppear),後兩個階段都屬於 main函數 執行階段。

pre-main 執行內容

此時對應的 App 頁面是閃屏頁的展示。

  • 載入可執行文件

    載入 Mach-O 格式文件,既 App 中所有類編譯後生成的格式為 .o 的目標文件集合。

  • 載入動態庫

    dyld 載入 dylib 會完成如下步驟:

    1. 分析 App 依賴的所有 dylib。
    2. 找到 dylib 對應的 Mach-O 文件。
    3. 打開、讀取這些 Mach-O 文件,並驗證其有效性。
    4. 在系統內核中註冊代碼簽名。
    5. 對 dylib 的每一個 segment 調用 mmap()。

    系統依賴的動態庫由於被優化過,可以較快的載入完成,而開發者引入的動態庫需要耗時較久。

  • Rebase和Bind操作

    由於使用了ASLR 技術,在 dylib 載入過程中,需要計算指針偏移得到正確的資源地址。 Rebase 將鏡像讀入記憶體,修正鏡像內部的指針,消耗 IO 性能;Bind 查詢符號表,進行外部鏡像的綁定,需要大量 CPU 計算。

  • Objc setup

    進行 Objc 的初始化,包括註冊 Objc 類、檢測 selector 唯一性、插入分類方法等。

  • Initializers

    往應用的堆棧中寫入內容,包括執行 +load 方法、調用 C/C++ 中的構造器函數(用 attribute((constructor)) 修飾的函數)、創建非基本類型的 C++ 靜態全局變數等。

main函數代理執行內容

從 main() 函數開始執行到 didFinishLaunchingWithOptions 方法執行結束的耗時。通常會在這個過程中進行各種工具(監控工具、推送、定位等)初始化、許可權申請、判斷版本、全局配置等。

首屏渲染執行內容

首屏 UI 構建階段,需要 CPU 計算佈局並由 GPU 完成渲染,如果數據來源於網路,還需進行網路請求。

優化方案

pre-main階段

檢測方法

獲得 main() 方法執行前的耗時比較簡單,通過 Xcode 自帶的測量方法既可以。將 Xcode 中 Product -> Scheme -> Edit scheme -> Run -> Environment Variables 將環境變數 DYLD_PRINT_STATISTICS 或 DYLD_PRINT_STATISTICS_DETAILS 設為 1 即可獲得執行每項耗時:

// example 
// DYLD_PRINT_STATISTICS
Total pre-main time: 383.50 milliseconds (100.0%)
         dylib loading time: 254.02 milliseconds (66.2%)
        rebase/binding time:  20.88 milliseconds (5.4%)
            ObjC setup time:  29.33 milliseconds (7.6%)
           initializer time:  79.15 milliseconds (20.6%)
           slowest intializers :
             libSystem.B.dylib :   8.06 milliseconds (2.1%)
    libMainThreadChecker.dylib :  22.19 milliseconds (5.7%)
                  AFNetworking :  11.66 milliseconds (3.0%)
                  TestDemo :  38.19 milliseconds (9.9%)

// DYLD_PRINT_STATISTICS_DETAILS
  total time: 614.71 milliseconds (100.0%)
  total images loaded:  401 (380 from dyld shared cache)
  total segments mapped: 77, into 1785 pages with 252 pages pre-fetched
  total images loading time: 337.21 milliseconds (54.8%)
  total load time in ObjC:  12.81 milliseconds (2.0%)
  total debugger pause time: 307.99 milliseconds (50.1%)
  total dtrace DOF registration time:   0.07 milliseconds (0.0%)
  total rebase fixups:  152,438
  total rebase fixups time:   2.23 milliseconds (0.3%)
  total binding fixups: 496,288
  total binding fixups time: 218.03 milliseconds (35.4%)
  total weak binding fixups time:   0.75 milliseconds (0.1%)
  total redo shared cached bindings time: 221.37 milliseconds (36.0%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load:  43.56 milliseconds (7.0%)
                         libSystem.B.dylib :   3.67 milliseconds (0.5%)
               libBacktraceRecording.dylib :   3.41 milliseconds (0.5%)
                libMainThreadChecker.dylib :  21.19 milliseconds (3.4%)
                              AFNetworking :  10.89 milliseconds (1.7%)
                              TestDemo :   2.37 milliseconds (0.3%)
total symbol trie searches:    1267474
total symbol table binary searches:    0
total images defining weak symbols:  34
total images using weak symbols:  97

優化點

  • 合併動態庫,並減少使用 Embedded Framework,即非系統創建的動態 Framework,如果對包體積要求不嚴格還可以使用靜態庫代替。

  • 刪除無用代碼(未使用的靜態變數、類和方法等)並抽取重覆代碼。

  • 避免在 +load 執行方法,使用 +initialize 代替。

  • 避免使用 attribute((constructor)),可將要實現的內容放在初始化方法中配合 dispatch_once 使用。

  • 減少非基本類型的 C++ 靜態全局變數的個數。(因為這類全局變數通常是類或者結構體,如果在構造函數中有繁重的工作,就會拖慢啟動速度)

main函數代理階段

檢測方法

  • 手動插入代碼計算耗時

    在 man() 函數開始執行時就開始時間:

    CFAbsoluteTime StartTime;  //  記錄全局變數
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            StartTime = CFAbsoluteTimeGetCurrent();
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }

    再在 didFinishLaunchingWithOptions 返回之前獲取結束時間,兩者的差值即為該階段的耗時:

    extern CFAbsoluteTime startTime; // 申明全局變數
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
        //...
        //...
    
        double launchTime = (CFAbsoluteTimeGetCurrent() - startTime);
        return YES;
    }

    通過這種手動埋點的方式也可以對每個函數進行埋點獲取耗時,但當函數多時也需要不小的工作量,且後續上線還需要移除代碼,不可復用。

  • Time Profiler

    Xcode自帶的工具,原理是定時抓取線程的堆棧信息,通過統計比較時間間隔之間的堆棧狀態,計算一段時間內各個方法的近似耗時。精確度取決於設置的定時間隔。

    通過 Xcode → Open Developer Tool → Instruments → Time Profiler 打開工具,註意,需將工程中 Debug Information Format 的 Debug 值改為 DWARF with dSYM File,否則只能看到一堆線程無法定位到函數。

 

通過雙擊具體函數可以跳轉到對應代碼處,另外可以將 Call Tree 的 Seperate by Thread 和 Hide System Libraries 勾選上,方便查看。

 

正常Time Profiler是每1ms採樣一次, 預設只採集所有在運行線程的調用棧,最後以統計學的方式彙總。所以會無法統計到耗時過短的函數和休眠的線程,比如下圖中的5次採樣中,method3都沒有採樣到,所以最後聚合到的棧里就看不到method3。

 

我們可以將 File -> Recording Options 中的配置調高,即可獲取更精確的調用棧。

 
  • System Trace

    有時候當主線程被其他線程阻塞時,無法通過 Time Profiler 一眼看出,我們還可以使用 System Trace,例如我們故意在 dyld 鏈接動態庫後的回調里休眠10ms:

    static void add(const struct mach_header* header, intptr_t imp) {
        usleep(10000);
    }
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            _dyld_register_func_for_add_image(add);
        });
      ....
    }
 

可以看到整個記錄過程耗時7s,但 Time Profiler 上只顯示了1.17s,且看到啟動後有一段時間是空白的。這時通過 System Trace 查看各個線程的具體狀態。

 

可以看到主線程有段時間被阻塞住了,存在一個互斥鎖,切換到 Events:Thread State觀察阻塞的下一條指令,發現0x5d39c 執行完成釋放鎖後,主線程才開始執行。

 

接著我們觀察 0x5d39c 線程,發現在主線程阻塞的這段時間,該線程執行了多次10ms的 sleep 操作,到此就找到了主線程被子線程阻塞導致啟動緩慢的原因。

 

今後,當我們想更清楚的看到各個線程之間的調度就可以使用 System Trace,但還是建議優先使用 Time Profiler,使用簡單易懂,排查問題效率更高。

  • App Launch

    Xcode11 之後新出的工具,功能相當於 Time Profiler 和 System Trace 的整合。

  • Hook objc_msgSend

    可以對 objc_msgSend 進行 Hook 獲取每個函數的具體耗時,優化在啟動階段耗時多的函數或將其置後調用。實現方法可查看 通過objc_msgSend實現iOS方法耗時監控

優化點

  • 通過檢測工具找到耗時多的函數,拆分其功能,將優先順序低的功能延後執行。
  • 梳理業務邏輯,把可以延遲執行的邏輯,做延遲執行處理。比如檢查新版本、註冊推送通知等邏輯。
  • 梳理各個二方/三方庫,找到可以延遲載入的庫,做延遲載入處理,比如放到首頁控制器的viewDidAppear方法後。

首屏渲染階段

檢測方法

記錄首屏 viewDidLoad 開始時間和viewDidAppear 開始時間,兩者的差值即為整個首屏渲染耗時,如果要獲得具體每個步驟耗時,則可同main函數代理階段使用 Time Profiler 或 Hook objc_msgSend

優化點

  • 使用簡單的廣告頁作為過渡,將首頁的計算操作及網路請求放在廣告頁展示時非同步進行。
  • 涉及活動需變更頁面展示時(例如雙十一),提前下發數據緩存。
  • 首頁控制器用純代碼方式來構建,而不是 xib/Storyboard,避免佈局轉換耗時。
  • 避免在主線程進行大量的計算,將與首屏無關的計算內容放在頁面展示後進行,縮短 CPU 計算時間。
  • 避免使用大圖片,減少視圖數量及層級,減輕 GPU 的負擔。
  • 做好網路請求介面優化(DNS 策略等),只請求與首屏相關數據。
  • 本地緩存首屏數據,待渲染完成後再去請求新數據。

其它優化

二進位重排

去年年底二進位重排的概念被宇宙廠帶火了起來,個人覺得噱頭大於效果,詳細內容可參考文章

總結

啟動優化不應該是一次性的,最好的方案也不是在出現才去解決,而應該包括:

  • 解決現存的問題
  • 後續開發的管控
  • 完整的監控體系

只有在開發的前中後同時介入,才能保證 App 的出品質量,畢竟開發是前人挖坑給後人填坑的過程

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、簡單介紹 SQLite 觸發器(Trigger)是資料庫的回調函數,它會在指定的資料庫事件發生時自動執行/調用。以下是關於 SQLite 的觸發器(Trigger)的要點: SQLite 觸發器(Trigger)可以指定在特定的資料庫表發生 DELETE、INSERT 或 UPDATE 時觸發, ...
  • 1. 2,點擊其他--新建目錄--輸入目錄路徑....dmp的目錄 3,新建一個表空間, 其他--表空間--新建表空間 點擊保存 4...點擊數據泵,,數據泵導入 5...點擊生成sql,運行, (運行裡面有ora報錯的話,自行百度) 6...創建一個用戶可能登陸的,用戶--用戶--新建用戶 7.. ...
  • 表結構 student(StuId,StuName,StuAge,StuSex) 學生表 teacher(TId,Tname) 教師表 course(CId,Cname,C_TId) 課程表 sc(SId,S_CId,Score) 成績表 問題十一:查詢至少有一門課與學號為“1001”的同學所學相同 ...
  • 1. 概述 Apache Cassandra將數據存儲在表中,每個表都由行和列組成。CQL(Cassandra查詢語言)用於查詢存儲在表中的數據。Apache Cassandra數據模型基於查詢並針對查詢進行了優化。Cassandra不支持用於關係資料庫的關係數據建模。Cassandra數據建模專註 ...
  • 從北斗衛星時鐘(北斗校時器)發展縱論世界衛星導航新格局 從北斗衛星時鐘(北斗校時器)發展縱論世界衛星導航新格局 更多資料添加京準電子科技官微——ahjzsz 世紀初,世界衛星導航領域建成並提供服務的衛星導航系統,主要有美國GPS、俄羅斯GLONASS和我國北斗衛星導航系統。其中,美國GPS系統向全球 ...
  • (一)跳躍表 跳躍表是一種有序的數據結構,它通過每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。 Redis使用跳躍表作為有序集合鍵的底層實現之一,如果一個有序集合包含的元素數量比較多,或者有序集合中元素的成員是比較長的字元串時,Redis就會使用跳躍表作為有序集合鍵的底層實現。 ...
  • 前言: 本文詳細介紹了 HBase QualifierFilter 過濾器 Java&Shell API 的使用,並貼出了相關示例代碼以供參考。QualifierFilter 基於列名進行過濾,在工作中涉及到需要通過HBase 列名進行數據過濾時可以考慮使用它。比較器細節及原理請參照之前的更文: " ...
  • 情況 App採用Glide做載入網路圖片功能。穩定版本的App,突然有很多圖片無法載入出來,經排查,除了Glide框架已經緩存過的圖片其他圖片都觸發了Glide.onError 異常為 大意是系統無法生成GlideModule。 我的編譯環境: 第一個解決方案: 無效 第二個解決方案 用 Java ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...