Trip-com APP 啓動優化實踐

Shanks,攜程移動開發專家,關注移動端基礎技術。

引言

啓動是用戶對 App 的第一印象,對於用戶體驗尤爲重要,所以我們花了很多時間在啓動時間的優化上。本文將分享 Trip.com App 的啓動優化實踐,從分析 App 啓動的過程開始,在瞭解啓動流程的基礎上制定大的優化原則和小的具體方案,希望能對大家有所幫助。

一、App 啓動的流程分析

想做啓動優化,首先要了解清楚啓動的各個流程,然後才能對各個環節去做針對性措施。

借用 WWDC 對啓動階段的定義圖:

1.1 System Interface

加載動態鏈接器dyld ,dyld會遞歸加載 App 依賴的動態庫,然後執行符號綁定RebaseBind。一般應用會加載 100 到 400 個 dylib 文件,幸運是大部分是系統庫,且系統會在操作系統啓動時計算和緩存系統動態庫。

Apple 爲了解決安全問題,引入ASLRCode Sign,如果不作符號修正,程序將沒法正常運行,所以會有 Rebase 和 Bind 過程。

在鏡像內部調整指針的指向,其實就是將內部指針都加上偏移量(Slide = 實際新地址 - 舊地址)

修正指外部的指針,比如上圖中 malloc,這個符號不存在於我們 App 的 Mach-O 中,需要從外部的鏡像中獲取,這時候就需要 Bind 操作把這個關聯起來。

調用系統的一些初始化方法,這部分一般時間比較固定,可以不用太關注。

1.2 Runtime Init

通過_dyld_objc_notify_register註冊回調,在 image 加載完時初始化語言相關。

在上面語言初始化完之後,會加載所有 category,處理 category 的所有方法,協議和屬性等。

也是通過向 dyld 註冊回調,在 image 加載完時,通過load_images 觸發,處理該 image 相關的所有 + load 方法,按照繼承層級依次調用:父類 + load→子類 + load→category +load,注意 category 的 + load 不會覆蓋原類。

1.3 UIKit Init

1.4 Application Init

這部分是我們熟悉的 UIApplicationDelegate 的幾個生命週期調用:

1.5 Initial Frame Render

這裏是 App 渲染第一幀,主要做了創建、佈局和繪製視圖的工作,並把準備好的第一幀提交給渲染層渲染。這裏面佈局計算,圖片解碼,圖層樹的遞歸 commit 到 Render Server 等都是可能影響耗時的點,所以要特別注意。

1.6 Extended

這裏按照蘋果的定義,是異步獲取數據展示界面的邏輯。比如我們首頁要從網絡請求數據然後展示最新數據在頁面上。

二、針對啓動的各個流程我們能做什麼

2.1 總體原則

刪的原則是指,對 App 啓動和運行不是必須的任務,或者跟首頁渲染第一幀無關的任務,都從啓動流程中刪除。對於刪除的任務,可以進行懶加載的形式,需要時再調用;也可以換到其他的時機去觸發,比如首頁渲染完之後。

壓的原則是指,對 App 啓動和運行必須的任務,或者直接影響首頁渲染第一幀的任務,都儘可能壓縮其運行時間。至於做法,可以是優化方法內的實現,使其運行更快;也可以將方法執行的線程切換到子線程,以併發的形式降低其對整個啓動過程的影響。

2.2 具體方案

2.2.1 減少動態庫

動態庫的加載在啓動階段是必須的,所以我們要儘量減少非必要的動態庫。對此我們做了以下幾點:

1)梳理所有動態庫,將用不到的或者可以簡單替代的動態庫刪除

可以通過otool -L xxx.app/xxx 或者打開打包後的產物,從 xxx.app/Frameworks 路徑中找到所有動態庫,逐個篩選,將其中可以廢棄和替代的動態庫刪除。

2)通過推進社區(第三方 SDK)將現有動態庫轉成靜態庫

因爲依賴了第三方 SDK,我們是不包含源碼的,所以這部分需要推進社區提供靜態庫的版本,或者通過 cocoapods 等工具打包 SDK 的靜態庫版本。

3)將我們自己的 SDK 編譯成靜態庫

對於我們自己的 SDK,因爲有源碼,所以直接修改MACH_O_TYPE 爲Static Library 重新打包即可。

4)App 最低支持系統版本升級到 12.2

因爲 iOS 在 12.2 版本及以上才內置了 Swift 的支持,所以在此之前 Swift 的動態庫都是隨着 App 下發的,也在 xxx.app/Frameworks 裏。

當然,這個決策是會直接應用到用戶和訂單的,所以是要有數據支持的,我們是根據用戶佔比到達某個閾值才支持 12.2 的。如果允許,甚至可以升級到 iOS 13,因爲 iOS13 以上 dlyd3 做了很多加載和緩存的優化。

2.2.2 刪除無用代碼

如果符號越多,很顯然 Rebase 和 Bind 的處理時間就會越長,Objc 的初始化也受影響,所以我們需要儘可能減少代碼:

1)通過逆向二進制或者生成 linkmap,解析所有方法(TEXT.text)和引用到的方法(__DATA _objcselrefs),找出無用方法刪除

2)解析所有類(DATA.objcclasslist)和引用到的類(DATA.objcclassrefs),找出無用的類刪除

3)使用第三方工具或者 clang 掃描重複代碼,精簡去重

4)使用LLVM_LTOGCC_OPTIMIZATION_LEVEL等其他編譯選項優化二進制大小

2.2.3 合併 category

合併 category,可以減少 category 加載時的耗時。不過這部分收益不大,並且也會影響編程習慣,所以我們並沒有投入很多時間,不再贅述。

2.2.4 刪除 + load

以前會有很多代碼爲了省事,加到了 + load 中,這部分很顯然佔用啓動時間,所以儘量要把這其中的代碼轉移,可以放到 initialize 中懶加載,或者放到啓動任務中併發執行,儘量減少這部分的影響。

Xcode 調試時,可以通過正則添加所有 + load 方法的斷點br s -r "\+\[.+ load\]$" ,然後使用br list打印出所有 + load 列表,這樣方便我們定位所有 + load。

2.2.5 UIApplication 子類優化

爲了減少 UIKit Init 的時間,可以對 UIApplication 的子類初始化工作優化。我們這部分不存在,所以沒有做什麼工作。

2.2.6 啓動任務併發

想象一下,如果application:didFinishLaunchingWithOptions:裏面執行的所有啓動任務不作任何處理,那麼代碼框架將會很亂,你的優化也只能單點單點去做。

所以我們將application:didFinishLaunchingWithOptions:階段所有方法任務化,一個任務做一種類型的事。任務拆分好之後,就可以根據任務之間的相關性,選擇哪些任務是可以併發執行,哪些任務是必須有依賴關係前後執行。

以前:

現在:

當然,任務的拆分顆粒度也很重要,拆分太粗的話,很難達到最優的組合,可能一個任務裏的方法之間仍然有並行的空間。拆分太細的話,也有可能導致同一時間併發數太多,造成額外的線程切換開銷。

2.2.7 I/O 處理

尤其要注意啓動階段的 I/O,一般出現於讀取磁盤中的文件,比如配置文件等。

使用 Instrument→App Launch 去查看啓動過程就會發現,如果主線程執行出現很多灰色的塊,那就是 I/O,找到這些 I/O 產生的方法,儘量在子線程併發執行,避免阻塞主線程。

2.2.8 首頁數據的預加載和懶加載

首頁上有很多數據要加載,比如圖片、上次緩存在本地的數據等等,這些數據的加載如果在寫代碼時不作特殊處理,那會在主線程執行,不知不覺就會有很多耗時。

1)預加載

對首頁渲染必須的數據,比如一個 icon,或者一個翻譯的數據,我們通過在啓動任務(之前提到的拆分的併發任務)中新增加一個預加載啓動任務,專門負責在application:didFinishLaunchingWithOptions: 的過程中併發執行數據的獲取。因爲獲取數據大多比較耗時,所以放在子線程充分利用啓動階段的空閒。同時這類任務大多數是 I/O 操作,並不會佔用太多 CPU 資源。

更進一步,其實可以對首頁用到的資源在運行時作個標記,記錄到磁盤,下次啓動的時候讀取這個記錄,對用到的資源進行提前預加載,這樣避免 hard code 很多資源名在代碼中。

2)懶加載

首頁的數據往往很多,但並不是一開始要全部用到。可以對數據作區分,和第一屏展示無關的,使用懶加載,真正用到的時候再去加載。

2.2.9 二進制重排

1)page fault 

由於虛擬內存的機制,應用啓動時不會把所有數據加載到內存,而是以頁爲單位逐步從磁盤中加載,內存中的虛擬地址和磁盤中的物理地址有個映射關係。當程序執行時,如果發現要訪問的東西不在內存裏,就會觸發一次page fault ,去磁盤中加載新的一頁。

啓動階段有很多方法要調用,而這些方法在 Mach-O 中的位置又是在編譯時確認的。如果有 10 個方法剛好在不同頁,可能就要產生 10 次page fault 。

通過在 Other C Flags 中添加-fsanitize-coverage=func,trace-pc-guard 再通過__sanitizer_cov_trace_pc_guard記錄啓動階段所有方法的調用,再將這些寫入到. order 文件中,在 Xcode 的ORDER_FILE 設置中配置即可生效。

通過測試,我們的二進制重排大概優化 100-200ms。

2.2.10 其他通用手段

針對啓動任務和首頁渲染階段,通用的手段是通過 instrument,profile 出耗時長的任務,對任務針對性地做方法優化。如果有的方法是第三方庫的,那就需要推進社區去更新。我們在做的過程中給 Firebase 和 Google 的一些 SDK 提了很多 issue,對方開發人員配合很積極,對我們幫助很大。

三、成果如何

通過長期的優化,以上手段全部用完之後,我們的啓動時間從原來的 2 秒,優化到 1 秒以內。

總結

在優化啓動時間的過程中,我們的收穫不僅是對啓動時間的優化,也對系統的啓動機制有了更深的瞭解,同時優化了我們自己的代碼,使其變得更加更加健壯和高性能。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JN2ZTPSW6ntZkfnqUX5K-g