58同城 Android App啟動優化實踐

語言: CN / TW / HK

1.前言

2.專案背景

3.優化分析

4.監控方案

5.優化方案

6.優化結果

7.總結展望

01

前言

App 啟動是指使用者從 App 之外的場景進入到當前 App 中的過程,按照 App 的程序是否存在以及主 Activity 的生命週期狀態,App 啟動主要包括冷啟動、溫啟動和熱啟動三種。啟動優化主要是針對冷啟動過程,目標是減少使用者從桌面點選 icon 啟動 App 到展示出 App 主頁的首幀畫面或者從其他應用調起 App 首次啟動到展示出業務的落地頁首幀過程的耗時。

關於 App 啟動優化的原理和檢測工具的介紹,網上已經有很多分享的資料,有的分享技術深入內容全面但理論性太強不便於在專案中實踐,有的只講了某些方面的優化細節而不成體系。每個 App 都有自己特有的業務邏輯和程式碼實現,有必要針對自身 App 的特點,系統地把細碎的優化方法組織起來,形成一套適合本 App 維護的完整的優化方案體系。本文將主要介紹我們團隊在 58同城 App 中進行啟動優化的實踐。

02

專案背景

隨著業務需求不斷迭代 App 內的程式碼邏輯越來越複雜,啟動流程的邏輯也越來越複雜,導致 App 的啟動效能逐漸劣化。通過啟動流程的埋點簡單監測了一個線上版本的啟動時間,統計發現有大約 20% 的使用者啟動時間超過了5s,這對外部投放業務帖子的落地頁到達率產生了不利影響。業務線團隊找到我們無線團隊,要求優化58 App 的啟動時間。

58同城 App 中集合了招聘、汽車、房產和本地生活服務等業務模組,在啟動過程中會初始化各業務模組,同時還會初始化大量的三方和自研的 SDK;除了通過點選桌面 icon 的方式啟動, 58 同城 App 中大量的業務帖子落地頁可能通過外部應用調起直達,這就要求 58 App 的啟動優化既要關注正常流程的優化,還要關注外部調起過程的優化。

03

優化分析

在設計優化方案實施優化動作之前,需要先對 App 的現狀進行摸底分析,針對現在的效能瓶頸進行有效的治理,謀定而後動,爭取最大化的優化收益和投入產出比。

3.1 啟動流程分析

首先從工程程式碼對 58 App 的啟動流程做全路徑分析,58同城 App 與大多數同類型 App 的啟動路徑類似,啟動邏輯主要是從 Application 開始到 App 的首頁主 Activity onResume 生命週期方法被執行。

冷啟動的 App 程序由 zygote 程序 fork 出來後會執行 ActivityThread 的 main 方法,在該方法中執行 attach 方法,然後通過跨程序通訊觸發執行 bindApplication,這是被啟動 App 的 Application 開始執行的起點,我們在應用中首先能觸達到的方法是其 attachBaseContext 方法,一般應用層的業務初始化從這裡開始。

接下來是 installProvider 階段,一些三方 SDK 可能借助該時機進行初始化,58 App 對這一階段沒有特別處理。然後會執行到 Application 的 onCreate 方法,這是 58 App 中主要的業務初始化階段,包括三方 SDK、業務線 Lib 庫和公司以及部門自研的通用中介軟體的初始化,雖然已經將所有初始化模組進行了細粒度的任務化和非同步執行,但沒有做到按需和延遲初始化等。

執行了 Application onCreate 方法後,系統會調起應用的啟動 Activity,在 58App 的 LaunchActivity 中處理了啟動過程的引導頁、開屏廣告和 deeplink 的業務分發邏輯,這部分邏輯非常複雜。正常啟動流程接下來會進入到 58App 主頁面 HomeActivity 中,如果是外部調起的 deeplink 方式啟動會進入到業務的落地載體頁 Activity。接下來就是主頁或者落地頁佈局的構建和渲染,當完成首幀 View 顯示後,就完成了使用者可感受到的應用啟動過程。

在 58 App 啟動過程中還有一個重要的邏輯是隱私許可權檢查,並在應用首次啟動時在 LaunchActivity 之前彈出提示框要求使用者選擇是否同意隱私協議。這裡使用了反射的方法,通過 SharedPreferences 儲存了相關狀態值,這部分邏輯在 Application attachBaseContext 方法中執行,對啟動效能有必然的影響。

3.2 耗時歸因分析

所有的耗時都是因為程式碼執行時不合理地消耗了系統資源而產生的,耗時歸因分析就是要找出程式碼中不合理地消耗了系統資源的地方,消耗系統資源的方式包括佔用過多 CPU 時間、頻繁的 CPU 排程、I/O 等待和鎖搶佔等。

程式執行最根本的是需要得到 CPU 時間片,如果一個任務需要較多的 CPU 時間執行,那麼它將影響其他任務的執行,從而影響整體任務佇列的執行;執行緒切換涉及到 CPU 排程,而 CPU 排程會有系統資源的開銷,所以大量的執行緒頻繁切換也會產生巨大的效能損耗;IO 和 鎖的等待會直接阻塞任務的執行,不能充分地利用 CPU 等系統資源。

我們基於原來的啟動流程和啟動任務,通過 trace 打點分析所有的啟動任務,並將 58 App 的啟動過程劃分成3個階段:

  • Trace-T1,從 Application 的 attachBaseContext 方法開始到 LaunchActivity 的 onCreate 方法被呼叫;

  • Trace-T2,從 LaunchActivity 的 onCreate 方法開始執行到 HomeActivity 的 onCreate 方法被呼叫;

  • Trace-T3,從 HomeActivity 的 onCreate 方法開始執行到首頁 Fragment 的 onResume 方法結束。

通過 Profile 分析工具按上述執行階段詳細地分析啟動過程中的任務耗時,梳理出啟動過程的所有任務的具體耗時點。

04

監控方案

古人云“工欲善其事,必先利其器”,對於啟動效能優化來說,如果能獲取到各啟動階段以及所有啟動任務的執行耗時的視覺化資料,將有利於瞭解啟動任務的耗時情況,幫助分析優化方向,同時也可以做優化對比。

Android Studio 中的 Profiler 工具以及線上效能分析工具 Perfetto 等都可以做非常詳細的效能分析,但需要匯出 app 執行的 traces 檔案,然後載入到其中才能進行分析,不便於我們開發過程中快速地分析。我們希望可以在 App 執行後直接上報效能資料,然後在工具中可以直觀地展示出詳細的任務耗時和執行時序圖。

於是我們基於 Profiler 的 API 開發了線上視覺化效能監控工具,通過在客戶端埋點列印執行任務的起止時間,然後在啟動完成後(即主頁首幀繪製完成)上報蒐集的效能資料,在後端分析資料並以時序圖展示出任務的執行耗時。

目前這套視覺化的監控工具主要是團隊內部用於在開發和測試階段輔助分析優化點和優化結果,目前還沒有實現對線上版本的啟動過程監控,也沒有對外公開這套工具。不過對於測試階段的使用者啟動 58App,可以產出啟動時間的統計資料,如下圖所示。對於正式釋出的線上版本,現階段我們仍然採用對啟動過程的關鍵點進行埋點統計啟動耗時。

05

優化方案

根據我們對 58 App 啟動流程的具體任務耗時問題的分析和梳理,結合工程程式碼結構的特點,秉著最小化侵入業務線程式碼的原則和小步快跑的迭代開發思路,我們進行了以下優化實踐:重構元件化的任務啟動框架、延遲不必要的初始化任務、合併啟動頁與首頁邏輯、優化首頁佈局和相關邏輯、外部調起定製化啟動。下面將對這些實踐內容的具體優化方案分佈做詳細的闡述。

5.1 元件化的啟動框架

優化前 58 App 中已經使用了一套將初始化任務 Task 化的啟動任務管理框架,但該框架存在幾個問題:

1、建立一個新的任務後必須手動新增到啟動過程的任務佇列中,並需要配置所屬程序和優先順序;

2、由於啟動任務佇列是在主 Module 的 Application 類中管理的,程式碼隔離的業務線如果要新增啟動任務將比較麻煩;

3、應用主程序和子程序的任務管理全在 Application 類中管理,存在大量 if-else 控制邏輯。總的來說,該任務管理框架有違開放封閉和單一職責等設計原則,程式碼的擴充套件性和維護性都存在問題。

5.1.1 元件化啟動框架原理

針對原有任務管理框架的問題以及對啟動任務管理提出了新的需求,我們開發了一套新的元件化的啟動框架來重構原有的啟動任務。

新框架借鑑了 Jetpack WorkManager 庫的設計思想,將啟動任務抽象成 Worker 實體,並設計了 WorkGroup 的概念,任務在任務組中根據依賴關係形成任務鏈順序執行,所有啟動任務被構建成有向無環圖。通過啟動框架的 API 指定任務的執行執行緒,並在框架內統一管理 IO 和計算類執行緒池,一定程度上實現對專案的執行緒收斂。

任何一個任務都必須在一個特定的任務組中執行,任務組是在定義具體任務時指定的。在橫向上任務組可以定義在不同的程序中,實現不同程序的啟動任務邏輯隔離,將原先在同一個 Application 類中處理不同程序啟動邏輯的程式碼解藕;在程序執行的時間線上可以按 Application 的生命週期或者自定義的執行時機定義任務組,這樣可以根據任務的具體特點靈活地進行延遲執行。

5.1.2 元件化啟動框架使用

使用新的啟動框架定義任務通過繼承 Worker 類,並通過 @WorkScheduler 註解指定任務的名稱、執行的執行緒、所屬任務組和依賴的其他任務,定義任務的模板程式碼如下。

在獨立程序中啟動該程序內的任務,需要繼承 ProcessApplication 類,實現兩個 Application 生命週期的方法,並在這些方法中通過 WorkManager 物件找到相關任務組,然後啟動任務。

5.1.3 元件化啟動框架的收益

重構元件化的啟動框架是我們本次啟動優化的基礎,開發元件化的啟動框架給我們帶來了以下收益:

第一、通過將任務元件化地實現,各個業務線可以自行優化拆解本業務模組內的啟動任務,方便任務擴充套件和獨立維護;

第二、通過註解方式配置任務的執行環境,可以在編譯期完成啟動任務序列的建立而不必硬編碼實現;

第三、將原框架中通過一個 Application 類管理所有啟動任務的方式,改為通過註解指定程序並實現 Application 的生命週期的方式分別管理各程序的啟動任務,讓程序職責更明確,同時也提高獨立程序啟動任務的擴充套件性和可維護性。

5.2 延遲初始化部分任務

按照元件化啟動框架設計,我們將原有的啟動任務進行拆解重構。按照任務優先順序和依賴關係,將任務細化為必需在 Application 的 attachBaseContext、onCreate 階段執行的和可以延遲到首頁載入後執行的。

以主程序為例,我們定義了 MainProcessAttach、MainProcessCreate 和 MainProcessDelay 這幾個任務組,並在相關任務的註解引數中配置這些任務組名稱。

延遲任務的執行邏輯在 DelayTaskManager 類中管理,在 Application 的 onCreate 方法中初始化該管理類,在這個類的初始化方法中會註冊 Application.ActivityLifecycleCallbacks 監聽 Activity 的生命週期,然後監控 Activity 首幀 View 渲染完成,在該時機觸發延遲任務執行。

從以下監控時序圖可以看出,優化後一些任務在首頁首幀渲染完成後才開始執行。

5.3 合併啟動頁與首頁

優化之前,58 App 的啟動 Activity 是 LaunchActivity,從前面的優化分析中可知,優化前啟動 58 App 到首頁可見的流程中會啟動兩個 Activity。

其中 LaunchActivity 主要承載了開屏相關的邏輯,包括處理開屏的廣告和活動策略、使用者首次啟動載入功能引導圖、外部調起的業務邏輯分發等,LaunchActivity 的邏輯非常臃腫。

5.3.1 合併收益

將啟動頁 Activity 的邏輯和首頁 Activity 合併可以得到至少兩方面的收益:

  • 減少一次 Activity 的啟動過程,避免系統 startActivity 過程的效能損耗;

  • 利用處理開屏過程的時間,執行一些與首頁 Activity 強關聯的併發任務,例如首頁資料預載入。

啟動 Activity 的過程從應用程序執行 Context 的 startActivity 方法,然後通過 Instrumentation 的 execStartActivity 調起 AMS 執行真正的 startActivity 相關方法,在完成了目標程序和 Activity 棧檢測之後回撥到應用程序,通過 ActivityThread 執行目標 Activity 的建立和啟動,整個過程涉及到兩次跨程序呼叫以及在應用程序的主執行緒通過訊息機制執行 Activity 的生命週期方法。

在應用啟動過程中如果連續啟動多個 Activity,一方面會觸發多次跨程序通訊,另一方面會導致應用程序的主執行緒訊息佇列堆積大量訊息,容易造成主執行緒效能損耗。

將啟動頁和首頁合併後,開屏頁成為展示在首頁之上的一層 View,原先需要在開屏頁退出後才能執行的首頁非同步任務或者與首頁 Activity 關聯的任務,比如首頁金剛位和 Feed 流資料載入等,可以提前到開屏頁展示過程中執行,減少總體啟動耗時。

5.3.2 問題及解決方案

合併了啟動頁和首頁的邏輯後,對應用的啟動邏輯產生了一些新問題,主要有:

  1. 如何解決外部通過 LaunchActivity 名稱調起 58同城 App 的問題?

  2. 如何解決 HomeActivity 的啟動模式和任務棧管理的問題?

  3. 訊息調起和外部調起的邏輯分發問題

第一個問題比較好解決,有兩種方案可以使用:首先可以在 Manifest 檔案中通過 activity-alias 標籤配置 targetActivity 將 LaunchActivity 指向 HomeActivity 來解決,如下圖所示;其次可以將原來的 LaunchActivity 改為空實現並繼承自 HomeActivity。

第二個問題實際上關聯了一些在不同場景的啟動過程頁面跳轉的任務棧管理問題。

在合併啟動頁和首頁邏輯之前,LaunchActivity 和 HomeActivity 的啟動模式分別是 standard 和 singleTask,這種情況下能夠確保 HomeActivity 只有一個 例項,並且當我們通過 Home 鍵將應用退到後臺然後點選桌面圖示再進入應用時,能夠重新回到之前的頁面。

但在合併之後,HomeActivity 變成了應用的啟動 Activity,如果繼續使用 singleTask 這個啟動模式,當我們從二級頁面點選 Home 鍵退出後點擊 icon 再次進入時,我們將無法回到二級頁面,而會回到 HomeActivity 的首頁 tab,因此合併後的 HomeActivity 其 launchMode 將不能使用 singleTask。

經過調研,我們最終選擇在 Manifest 檔案中配置 HomeActivity 的 launchMode 為 standard。對於應用內啟動 HomeActivity 的場景,我們通過在startActivity 的 Intent 中增加 FLAG_ACTIVITY_SINGLE_TOP 和 FLAG_ACTIVITY_CLEAR_TOP 的 flag,以實現類似於 singleTop 啟動模式的特性,並實現 onNewIntent 方法執行一些頁面設定的邏輯。

對於從外部調起 App 進入業務落地頁的場景,我們設計了一個空實現的 Activity,並在 Manifest 中指定其 parentActivityName 屬性為 HomeActivity,然後通過 TaskStackBuilder 將該空 Activity 新增到待啟動的落地頁 Activity 的任務棧中,這樣可以從目標 Activity 回退時進入到 HomeActivity。

通過推送訊息調起 App 主要是修改啟動 Activity 的名稱,對於外部 deeplink 方式調起 58 App 的邏輯分發問題,主要考慮將原來依賴 LaunchActivity 分發的邏輯遷移到落地頁載體 Activity 中。

5.4 優化首頁佈局與邏輯

對首頁佈局的優化主要是減少首頁佈局樹的渲染時間,首頁的佈局主要是在 setContentView 方法中通過 LayoutInflate 去載入佈局 xml 檔案,佈局載入的過程主要包括 3 個步驟:

  1. 通過 XmlResourceParser 的 IO 過程將 xml 檔案解析到記憶體中;

  2. 根據 XmlResourceParser 的 Tag name 獲取 Class 物件的 Java 反射過程;

  3. 建立 View 例項,最終生成 View 樹。

載入佈局的整個過程比較耗時,58 同城 App 經過多年的業務迭代,佈局結構已經變得非常複雜。在佈局方面我們主要進行了以下優化動作:

首先,在佈局設計層面,通過使用 merge 標籤或者約束佈局等方式優化 xml 佈局層級,使用 ViewStub 標籤進行按需載入佈局。

其次,在業務層面,前期我們已經實現了將首頁佈局按模組進行封裝以便獨立載入,比如拆分出了搜尋框、活動位、金剛位和 Feed 流佈局等模組。

最後,我們在啟動優化實踐中實現了非同步執行緒預載入佈局的方案,通過自定義 AsyncLayoutInflater 將 MainLooper 設定到非同步執行緒上,解決解析 xml 生成 View 之後還需要 post 到主執行緒的問題。

另外,我們還進行了一些基礎邏輯的優化:

  • 治理子程序啟動邏輯,針對 WebView 的沙箱程序和其他子程序,將其啟動時機延遲到主程序主頁或目標落地頁渲染完成之後;

  • 根據網路請求耗時的監控,提高網路併發能力;

  • 優化網路請求 Header 的獲取,降低構造 Header 的耗時,提早網路請求的發起時機。

通過首頁佈局和邏輯的優化實現了進入首頁後更快地體驗到載入完成的介面內容,整體提升了 58 同城 App 的使用者體驗效果。

5.5 外部調起優化

58 同城有很多業務活動是通過投放三方平臺進行推廣的,使用者在其他三方平臺通過引導連結進入活動帖子詳情頁面,通過前面的分析知道,這個過程也會經歷 58 App 的啟動流程。如果從外部調起進入業務帖子落地頁的耗時較長,將會影響業務活動的使用者到達率,所以業務線非常看重這個啟動過程的效能問題。

通過外部調起時的跳轉協議可以明確是什麼業務線的帖子,58 同城 App 內的業務模組實現是相互獨立的,這樣可以根據調起的帖子型別確定啟動過程需求執行的初始化任務。根據外部調起的特點,我們設計了一個沙箱程序用於執行業務線定製化的啟動任務,可以在外部調起時只初始化與當前業務有關的任務,從而快速開啟業務落地頁,在沙箱程序中完成帖子頁面展示後會立即通知主程序啟動。

06

優化結果

經過前面分析設計的優化方案落地實踐,58 同城 App 的冷啟動時間總體平均提升50%左右。由於首次啟動過程會執行隱私許可權檢查和彈框的邏輯,並且隱私許可權彈框依賴使用者交 互,我們在測試啟動優化效果時分別就首次啟動和非首次的冷啟動場景進行優化前後的對比測試。

首次安裝啟動的效能優化對比圖(以下柱狀圖中縱座標單位均為毫秒 ms):

非首次啟動的冷啟動效能對比圖:

另外,我們還將優化後的 58 同城 App 與業內同類型的 App 同時期的官方版本進行了啟動效能體驗對比,以比較我們的優化效果相比業內優秀 App 的差距。這裡的效能對比只是從桌面點選應用圖示到各自首頁完成載入的啟動過程的感官體驗對比,在相同裝置上相同網路環境下分別啟動並錄製影片,然後對齊啟動動作首幀畫面,合成同步啟動對比影片,效果如下:

App 的啟動效能是一個綜合性的體驗指標,它不僅受 App 啟動流程的業務邏輯治理的影響,還會受到 App 包體積以及手機執行環境等的影響。針對上述比較的幾款 App 我們無法得知他們在啟動流程中做了哪些具體的優化策略,僅從包體積上比較,上述幾款應用中美團 App 的包體積相比其他幾個小大約 20MB,因此它的啟動速度也是肉眼可見最快的。

07

總結展望

以上我們的啟動優化實踐主要是基於業務層面的常規的系統性優化,優化結果基本達到了本階段的優化目標,但相對於業內具有更優秀的啟動體驗的 App 還有些許差距。效能優化本身就需要持續改進、日益精進,參考業內同行分享的優秀經驗,我們還有很多工作可以做。

7.1 持續迭代

目前我們已經實踐了的優化動作粒度相對較粗,帶來了不錯的收益可能得利於本階段進行了大量的對歷史程式碼結構的重構優化,但還有很多細粒度的效能問題沒有深入優化,比如 IO 優化、ContentProvider 優化、GC 優化、執行緒排程和鎖優化等。這些優化點也許不能帶來很大的優化效果,但是做好了這些方面將會給 App 整體效能帶來質的提升,當然做好這些優化不能一蹴而就,唯有通過持續迭代,一個點一個點地優化解決。

7.2 防劣化

攻城容易守城難,我們本次優化達到了不錯的效果,但隨著業務迭代,新增的程式碼邏輯有可能會抵消一些現在的優化收益,這就需要我們做好效能防劣化的工作。線下分析工具可以幫助我們定位問題點,分析產生效能問題的原因,並且可以快速對比優化結果;而線上效能監控有助於我們掌握效能變化的趨勢。更好地設計監控方案,可以幫助分析出效能優化或者劣化的原因,針對性地解決劣化的問題,從而保持啟動效能優化的狀態。

7.3 新方案探索

以上實踐的優化方案都是常規的優化思路,在將常規方案做到極致後,我們是否可以開拓思路探索新的優化方案呢?比如結合端智慧針對使用者畫像按使用者的常用功能進行啟動路徑優化。這關鍵點在於 App 的具體使用者畫像,比如使用者裝置效能,常用哪些功能以及不用哪些功能等。後端收到相關上報統計後,就可以給符合某些畫像的使用者下發最佳的啟動路徑,比如啟動的時候不初始化某些模組,或者 delay 初始化某些模組,或者增加對熱點模組的預熱等。

參考文獻

[1] 應用啟動時間:https://developer.android.com/topic/performance/vitals/launch-time?hl=zh-cn
[2] 抖音 Android 啟動優化理論:https://juejin.cn/post/7058080006022856735

作者簡介

  • 龐立:58集團-技術工程平臺群 Android工程師

  • 趙聰穎:58集團-技術工程平臺群 Android工程師

  • 孔校軍:58集團-技術工程平臺群 Android工程師