1. 程式人生 > 其它 >抖音 Android 效能優化系列:啟動優化之理論和工具篇

抖音 Android 效能優化系列:啟動優化之理論和工具篇

作者:抖音基礎技術團隊
來源:位元組跳動技術團隊

啟動效能是 APP 使用體驗的門面,啟動過程耗時較長很可能導致使用者使用 APP 的興趣驟減,抖音通過對啟動效能做劣化的 AB 實驗也驗證了其對於業務指標有影響顯著。抖音擁有數億的使用者,啟動耗時幾百毫秒的增長就可能帶來成千上萬使用者的留存縮減,因此,啟動效能的優化成為了抖音 Android 基礎技術團隊在體驗優化方向上的重中之重。

本文基於過往對抖音 Android 客戶端做啟動效能優化的實戰經驗總結提煉出普適性的方法論,並將該過程中沉澱的工具加以分享,希望能給大家帶來一些新的思考。

假如你要負責優化抖音的啟動效能,你會怎樣去規劃整體的優化方案?你可能會一下子想到很多方面的細節點,比如:要優化主執行緒耗時、要減少佈局層級、要對某些啟動任務做按需載入或預載入、要避免主執行緒 IO、要對執行緒使用進行優化、還要有分析工具幫助定位效能問題等…

然而,該如何系統性地把這些細碎點組織起來並按照一定的章法來落地啟動優化呢?此時,需要我們在具體細節點之上有進一步的問題分解與深入思考,最終形成一套完整的方法論,不僅能覆蓋所有細節點,還能切實指導在實戰中達成啟動優化的效果。切實有效的方法論必然是從實戰中經過千錘百煉才能形成的,而抖音龐大的使用者基數又進一步保障了方法論的可行性與普適性。那麼接下來讓我們帶著前述問題來看抖音的啟動優化方法論是怎樣的又是如何應用於實戰之中的。

啟動優化方法論

抖音的啟動效能優化方法論分為五部分,分別是:理論分析、現狀分析、啟動效能優化、線上驗證與防劣化。

這五部分間存在明顯的先後順序,又能閉環達成可持續的啟動效能優化,下面將對這五部分做詳細闡述:

理論分析

理論分析放在最先是為了從一開始就避免讓視野受到限制,很多同學往往一開始接手啟動優化就容易陷入對各種現狀細節的分析,拘泥於片面的潛在可優化點,這樣就難以做到對全域性和優先順序的把控,所以,我們應該首先跳出現狀,從更加全域性的視角來思考整體優化的目標和策略。這裡可以利用特斯拉創始人——埃隆·馬斯克所推崇的“第一性原理”思考法:

“通過第一性原理,把事情昇華到最根本的真理,然後從最核心處開始推理。”

基於此,我們在做啟動優化的理論分析時可以從更本源的角度出發做到全域性思考,比如抖音會做從程序建立到頁面展示的全啟動路徑分階段耗時分析、還會按照消耗的系統資源型別做耗時成因分析,通過這種極致的耗時分析可以帶來極致的優化策略,此外,從全路徑

出發還能夠發現容易忽視的問題、探索優化的極限。

現狀分析

在完成理論分析後,我們基本具備了全域性的視角,並且也大致清楚了整體的優化目標和策略,接下來就要基於此來做現狀分析從而明晰實現目標的具體路徑:

  • 首先使用 profile 工具對可優化點進行摸底:其實不合理的高耗時點就是潛在的優化點,並能按照前述的理論分析歸入一個或多個耗時成因中;
  • 然後結合線上的指標資料確定最終優化方向:線下摸底的潛在優化點要結合其線上打點確認是否為普遍耗時,再根據耗時成因明確大致的優化思路、實施成本和預估收益。

在這部分需要尤其注意三點:優質的 profile 工具(這裡推薦使用同樣來自基礎技術團隊的“新一代全能型效能分析工具”)、線下 trace 結合線上監控綜合分析、根據投入產出比評估實施優先順序,這三點是保障切實有效取得啟動優化收益的關鍵。

啟動優化

在完成了理論和現狀分析後,就可以根據規劃的路徑來實施具體的啟動優化項了。在實施過程中,主要考慮主執行緒優化、後臺執行緒優化和全域性優化三個維度:

  • 主執行緒耗時優化需要在啟動全路徑各階段中細化具體的耗時成因,如:CPU Time、CPU Schedule、IO wait、Lock wait 等,完成耗時歸因後可以使用逐步升級的優化策略來逐個擊破:對於首屏所必須的耗時邏輯做正面優化(可使用縮減耗時邏輯、非同步併發、延遲載入等手段)、對於非首屏必須的耗時邏輯做按需載入(需要架構優化的基礎)、對於優化後仍存在耗時的邏輯嘗試做業務降級(大都有損需評估全域性收益);
  • 後臺執行緒優化策略與主執行緒類似,在此基礎上還可以實施後臺任務縮減、執行緒收斂、開啟多程序等優化措施;此外,主執行緒和後臺執行緒均存在較多啟動任務且彼此間可能存在關聯,因此,可以對全域性的啟動任務做依賴關係梳理並實施精細化的任務重排,旨在減少依賴任務間的等待耗時;
  • 全域性優化主要是指業務無關的通用的全域性優化策略,如虛擬機器層面或 IO 層面的優化等。

線上驗證

在完成了具體的優化項施工後,就來到了線上驗證大盤收益的階段。這個階段有三點需要注意:

  • 線下的優化一定要有線上的指標反饋,線下的優化項因為裝置或操作習慣差異往往難以評估是否具備普遍影響,只有當相應的線上指標取得正面反饋後才能驗證拿到了有效的優化收益;
  • 線上指標需要結合均值與分位值綜合來評估,只關注啟動耗時的均值往往會掩蓋低分位裝置的現狀,這部分裝置可能佔比不高,對均值影響有限,但抖音龐大的使用者基數乘以該比例仍舊是不小的數量,為了保障該部分使用者的啟動效能體驗,抖音一般會分 50%、70%、90%三個分位值來評估指標;
  • 在驗證收益時通過 AB 實驗達成,這樣做不僅能控制變數確保優化項的嚴格有效,還能借此來觀察效能優化所帶來的業務指標收益,這些都可以作為規劃後續啟動優化方向的參考指導。

防劣化

在線上驗證優化措施取得切實收益後,並不是萬事大吉了,持續保持住優化效果才算完整達成了啟動效能優化的目的。其實不僅是啟動優化,整個效能優化領域都是圍繞著“”和“”來展開的,“攻”即為前述的分析與優化,而“守”則是防止劣化,在防劣化方面大家往往不會像優化的方面那麼重視,但實際上能防止劣化是可持續取得優化效果的前提(否則新的優化效果會用於彌補劣化甚至入不敷出),並且防劣化相比於優化是更能持久有益的

抖音啟動效能防劣化的程序分為了三個時期,不同時期有不同的表現與應對手段,這很可能是大多數 APP 優化啟動效能都要經歷的,這裡提煉出來以供參考:

  • 快速下降期:此時一般位於啟動優化的初始階段,優化空間很大,伴隨有小幅度的劣化但往往都能被更大幅度的優化抵消且還仍有收益,這時應該抓大放小,按照更高投入產出比的策略重點推進優化,同時也抽出少部分精力治理修復成本低的劣化。
  • 瓶頸期:到了該時期絕大部分優化收益已經拿到,想進一步做到優化往往需要投入更多成本,且優化幅度有限,整體的投入產出比不高,同期還會伴隨有中小幅的劣化,此時需要建立完善的線上線下監控體系,及時發現並修復劣化,此外還要通過架構改造從源頭上限制劣化的發生,綜合保障優化的收益不會被劣化抵消。
  • 劣化期:這個時期往往出現在年關或重要節日期間,這類時間點往往有重要且緊急的活動專案上線,眾多關聯方面均要為其開綠燈,啟動效能指標也不例外,為了保障活動效果可能要加入若干耗時的主執行緒啟動任務,所帶來的的劣化幅度往往比較大,此時需要對齊預期並在活動結束後及時修復。

啟動優化方法論的應用實踐

古人云“紙上得來終覺淺,絕知此事要躬行”,前述的方法論講得再詳細再透徹也會與實際的落地存在隔閡,為了做到真正的學以致用,下文將細緻講解如何將啟動優化方法論應用於實踐之中。

理論分析的實踐

抖音在理論分析部分會對啟動流程分別作全路徑分析和耗時成因分析,前者用於發現全路徑各個階段的潛在耗時點避免疏漏,後者用於系統性地將各個耗時點歸因從而引導我們找尋優化思路,關於這兩部分的具體實踐如下:

啟動效能全路徑分析:抖音的啟動路徑和大多數 APP 類似,整體分為兩大階段和兩個間隙,它們按時間順序排布為:Application 階段、handle message 間隙、Activity 階段和資料載入間隙,全路徑各部分細分涵蓋的內容如下圖所示:

APP 程序由 zygote 程序 fork 出來後會執行 ActivityThread 的 main 方法,該方法最終觸發執行bindApplication,這也是 Application 階段的起點;然後是我們在應用中能觸達到的attachBaseContext階段,4.x 的機型在該階段具有較長的 MultiDex 耗時可以做針對性優化(可參考“抖音 BoostMultiDex 優化實踐”),本階段也是最早的預載入時機;接下來是installProvider階段,很多三方 sdk 藉助該時機來做初始化操作,很可能導致啟動耗時的不可控情形,需要按具體 case 優化;此後就到了 Application 的onCreate階段,這裡有很多三方庫和業務的初始化操作,是通過非同步、按需、預載入等手段做優化的主要時機,它也是 Application 階段的末尾。

Application 階段和 Activity 階段之間往往會不可避免地被插入很多 post 到主執行緒的訊息及相應待執行任務,這是拉長啟動耗時的另一不可控問題點,需要加以監控治理或通過訊息排程優化來儘量減小此間隙。

在來到 Activity 階段後,首先經歷的是其onCreate生命週期,這裡涵蓋了首屏業務優化的主要場景也是開啟非同步併發的主要時機,在其中有個重要的 setContentView 方法會觸發 DecorView 的 install,可嘗試對 DecorView 的構建進行預載入;後續自然來到View 構建的階段,該階段在抖音上相當耗時,可採用非同步 Inflate 配合 X2C(編譯期將 xml 佈局轉程式碼)並提升相應非同步執行緒優先順序的方法綜合優化;再來到View 的整體渲染階段,涵蓋 measure、layout、draw 三部分,這裡可嘗試從層級、佈局、渲染上取得優化收益。

最後是首屏資料載入階段,這部分涵蓋非常多資料相關的操作,也需要綜合性優化,可嘗試預載入、快取或網路優先順序排程等手段。

此外,針對全路徑所有階段還可以實施通用性的優化項,如:啟動任務排程框架、類重排、IO 預載入、全域性通用性框架優化等。

啟動耗時成因分析:所有的耗時均因程式碼執行時不合理地消耗系統資源產生,而不合理的耗時點正是需要做歸因分析之處。抖音按照不合理耗時點消耗的主要系統資源型別劃分出五大成因,分別是:CPU Time、CPU Schedule、IO Wait、Lock Wait 和 IPC,下面分別對各成因進行剖析:

  • CPU Time 指佔用 CPU 進行計算所花費的時間絕對值,中斷、掛起、休眠等行為是不會增加 CPU Time 的,所以因 CPU Time 開銷佔比高導致的不合理耗時點往往是邏輯本身複雜冗長需要消耗較多 cpu 時間片才能處理完。比較常見的高 CPU 佔用是迴圈,比如抖音啟動時遇到過一個 so 載入耗時,最後定位原因是在解壓 so 的時候,遍歷 ZipEntry 的次數過多導致,一個可行的優化策略就是可以把 so 所在的 ZipEntry 提前,遍歷完 so 的 ZipEntry 之後可以提前中止遍歷,而不需要遍歷剩下的無效 ZipEntry。除迴圈之外,反射也是導致 CPU Time 的重要原因,像在序列化/反序列化、View Inflate 時,都有大量的反射操作,反射的耗時主要是字串去查詢 Method 或者 Field,這個優化策略也可以考慮提前查詢 Method 和 Field 快取起來,或者是通過內聯來降低 Field 數量等。另外一個常見的 CPU 耗時是類載入,類的載入過程包括:Load,從 Dex 檔案裡讀取類的資訊,可通過類重排優化;Verify,驗證指令是否合法等,通過關掉 Class Verify 可以優化該過程,同時高版本的 vdex 也是為了優化 verify 過程而設計,在 dex2oat 的時候做 verify,verify 之後的結果儲存成 vdex,後續只需要載入 vdex;Link,給 Field、Method 分配記憶體,按照名字排序以方便後續反射的時候查詢 Field、Method 等,這個過程的優化,art 虛擬機器採用了 ImageSpace 的方案進行了優化,將 Link 後的記憶體儲存為 image 檔案,後續可以直接 load 這個 image 檔案,省去了 Link 過程;Init,類的初始化。
  • CPU Schedule 在分析時主要針對主執行緒,是指主執行緒處於可執行狀態但獲取不到 cpu 時間片,這類耗時可能和執行緒排程等有關,最終導致分配給主執行緒的 cpu 時間片不足以及時處理完其內任務。由於主執行緒的執行緒優先順序比其他執行緒的優先順序要高很多,通常影響並不大,事實上抖音做了線上使用者的啟動耗時統計,這部分的耗時佔比也是不大的。不過有一個場景需要關注,就是渲染,渲染是需要 RenderThread 提交 GPU 的渲染命令,而 RenderThread 並沒有主執行緒那麼高的優先順序,因此比較容易受 CPU 的負載的影響,導致渲染耗時,這個對於啟動來說影響並不算大,啟動只有一次首頁的渲染,佔整體時間的比例不算大,但對於流暢度的影響就會比較大。這類耗時的優化主要還是從降低 CPU 的負載的角度考慮,比如業務降級、業務打散等手段。抖音還通過對 RenderThread 優先順序的提升優化,拿到了不錯的收益。
  • IO Wait 指發生了 IO 操作需要等待 IO 返回結果,這類耗時可能發生在讀取資源和檔案,類載入,甚至在記憶體不足時的 PageFault 都會導致 IO Wait。Resources 的相關的操作耗時,主要是需要從 apk 裡讀取資原始檔,優化策略可以有預載入、資源重排、資源非同步載入等。類載入的 IO Wait 和 Resources 類似,也可以通過類的重排、預載入等優化方案。檔案讀寫導致的 IO Wait 又分為業務檔案和系統檔案,業務檔案指業務邏輯的讀寫檔案,一般都可以通過非同步來解決,而系統檔案的例子是 dex 的讀寫,抖音的 IO Wait 很大一塊是它貢獻的,目前的思路還是做 dex 的重排和 IO 的預讀來嘗試優化。
  • Lock Wait 也是主要針對主執行緒,指其處於等鎖狀態,等待被其他執行緒喚醒或自己超時喚醒,導致這類耗時的問題種類多樣,大體也是可以分為業務鎖和系統鎖,業務鎖主要是被主執行緒等待的業務邏輯未能及時處理完,優化思路一般是移除主執行緒的鎖等待邏輯或者加快被等待的業務邏輯的執行速度。系統鎖主要有:String InternTable Lock,ClassLinker Lock,GC Wait Lock 等,目前抖音正在嘗試優化這幾類的鎖耗時。
  • IPC 指程序間通訊,作業系統大都含有相應的機制,Android 中所特有的 IPC 機制是 Binder,由於進行 IPC 呼叫往往需要等待通訊結果本質上這也算是一種 Lock Wait,但 Android 特有 Binder 機制所以單獨列出,這類耗時可採用減少或替代 Binder 呼叫等手段來優化。
    綜合前述的五大耗時成因,這裡舉一個分析啟動階段 UI 耗時成因的例子作為實踐參考,根據 UI 介面的生命週期(一般劃分)——UI 構建、資料繫結、View 顯示三個階段分別進行分析:

綜合前述的五大耗時成因,這裡舉一個分析啟動階段 UI 耗時成因的例子作為實踐參考,根據 UI 介面的生命週期(一般劃分)——UI 構建、資料繫結、View 顯示三個階段分別進行分析:

  • UI 構建階段中首先要對介面佈局的 xml 檔案進行解析,這會導致 IO Wait 耗時,在接下來要解析 xml 檔案中的 TagName 從而獲取對應 View 的 class 會用到反射、建立各子 View 例項並生成 View 樹又會用到迴圈遞迴,兩部分都會增加 CPU Time 的開銷。
  • 然後是資料繫結階段,該階段主要分兩部分,一部分是對資料做請求、解析、適配,另一是部分是將適配好的資料填充進 UI 中,前一部分往往會涉及到 Json 解析成 Data Class 例項,這裡就可能涉及反射、迴圈遍歷巢狀的資料類結構等增加 CPU Time 的操作。
  • 最後是View 顯示階段,常見的 measure、layout、draw 三大渲染 View 的步驟就在其中,它們同樣會產生遞迴遍歷父子 View 的耗時,此外這裡還涉及將應用層計算好的渲染 View 的資料傳遞給系統層做最終的畫素點排布,那麼必然又會產生 IPC 耗時。

從這個例子可見即使再複雜的場景只要我們進行細粒度的分析,都能將耗時點歸入前述某一成因中。

現狀分析的實踐

如前文方法論所述,現狀分析包括線下 Profile 資料與線上監控資料的對照分析,綜合這兩部分可以明確切實影響大盤啟動效能的普遍耗時點,從而確保要做的優化項是行之有效的。下面分別講述這兩部分資料的分析實踐:

線下 Profile 資料分析:Profile 主要是指使用效能探測工具抓取應用啟動路徑各階段的耗時和系統資源消耗情況,常見的開源 Profile 工具有 TraceView、Systrace、Android Profiler 等,這些工具各有優勢但均不能完全滿足抖音做線下 Profile 的需求(詳見後文“啟動效能優化工具”部分的講解),為此,抖音自研了“新一代全能型效能分析工具 RheaTrace”滿足了需求。通過該工具我們可以線上下抓取整個啟動路徑的 Trace 檔案,其整體樣式與 Systrace 一致,但是涵蓋了更多的資訊點,一個樣例 Trace 檔案如下圖所示:

這裡需要注意抓取 Trace 一定要基於 release 包,debug 包中往往涵蓋諸多除錯邏輯可能影響啟動效能,導致 profile 資料與實際使用情形存在偏差。在檢視 profile 資料時,首要觀察主執行緒,尋找其中不符合預期的耗時方法,抖音將主執行緒耗時在 5ms 以上的方法均認定為不符合預期;然後在所有不符合預期的方法中尋找 Top n 的耗時點,逐個分析耗時原因、尋找突破口;耗時原因需要結合方法實現邏輯以及諸多執行時資訊綜合分析(這裡可以參考 Google 官方文件“瀏覽 Systrace 報告”),需要關注的執行時資訊有方法執行時段對應的 CPU 負載、執行緒狀態的顏色標識值、鎖資訊、IO 耗時、Binder 呼叫耗時等,根據這些資訊判定引起方法耗時的主要原因,再結合理論分析中不同階段、不同系統資源型別探尋優化手段。

線上監控資料分析:這部分資料的分析主要是用作參照和補充,參照是指線下 Profile 資料分析出的耗時點要對照線上資料確認其在大盤中存在普遍耗時,補充是指線下 Profile 資料未能復現的耗時點可能存在於線上大盤中,這部分漏掉的耗時點需要線上下嘗試復現、歸因後實施優化。這裡有個很重要的點是:該如何對線上的啟動效能指標做監控,這是保障線上資料能真實反映使用者體驗並且與 QA(做競品測試等)和業務方(判斷業務需求是否影響啟動效能等)達成一致的前提,下面將對這部分做詳細闡述,分為啟動效能指標的定義、統計和校準三部分:

  • 啟動效能指標定義:啟動指標定義主要在於如何確定啟動路徑的起點與終點。起點的備選項有下圖中的三個點以及 Application 的 attachBaseContext 方法:
  • 下圖中“點選圖示”後的/proc/self/stats starttime 是核心中記錄的 App 啟動時間點,該資料的 Android 版本相容性良好也比較貼合真實情況,可以作為備選;
  • 接下來的 Process.getStartElapsedRealTime 是 Framework 中記錄的 APP 程序建立起點,該 API 是 Android N 起才提供的相容性較差;
  • 再往後是 Application 的建構函式,按照 Android 官方生命週期式的開發模式通常不會往 Application 的建構函式中加邏輯,所以不建議在這裡記錄起點;
  • 最後是大家熟悉的 attachBaseContext 方法也是 Application 生命週期中非常早的一個點,可以在這裡記錄啟動點,雖然可能和真實情況有小幅差距,但能夠起到基本的對照效應並且處於 APP 邏輯可以干預的範圍內,抖音的啟動路徑起點選定的正是此處,此外也可以結合前述的核心中啟動時刻綜合觀測。
  • 而關於終點的定義同樣有幾個備選項:Activity-onResume、Activity-onWindowFocusChanged、View-dispatchDraw 和 DecorView-post:

  • 少數 APP 在選定冷啟階段終點時可能會選擇首頁 Activity 的 onResume 時機,但此時整個 Activity 還不是完全可見,與使用者感受到的冷啟階段結束時刻有一定的差距;

  • 基於上述原因,更多 APP 會選擇首頁 Activity 的 onWindowFocusChanged 時機,抖音也是選擇的此時機作為冷啟過程的終點,此時首頁 Activity 已經可見但其內部的 view 還不可見,對於使用者側已經可以看見首頁背景,即認為冷啟階段到此結束,後續的首頁內 View 繪製歸入首刷過程中;

  • dispatchDraw 從 View 可見這個角度講應該是比較接近使用者感受的,但其受業務改動影響較大,不利於把控冷啟時間及維護;

  • 最後是通過 DecorView 在 attachToWindow 前 post 一個 runnable 來打點的方式,該方式可以保障在業務 View 完成渲染後做打點,但該方式可能會受業務同學做懶載入在打點前插入邏輯的影響,因此抖音的冷啟終點也未選用該時機。

  • 啟動效能指標統計:在統計效能指標時有個關鍵點往往被大家忽略,就是分位值的概念,由於平均值相對更通俗易懂且對大盤突發問題敏感往往作為首要統計指標被關注,但其存在波動較大不利於大盤監控以及難以體現不同分位機型啟動效能差異的問題,而分位值更有利於全面監控且各分位波動相對較小,此外對於低端機的效能問題能夠更好地顯現出來,有助於做專項優化,在優化抖音的啟動效能時我們會重點關注 50 分位和 90 分的效能指標,不過分位值也存在一些缺點,比如:概念理解起來相對複雜、個別 bad case 分散到各分位不容易體現出來等,因此比較好的實踐是:日常優化主要統計分位值,平均值作為輔助完善監控體系。

  • 啟動效能指標校準:由於啟動路徑往往比較複雜,因此添加了啟動效能埋點後還需要額外的校準,總的原則是需要保障指標資料能切實反映大盤使用者情形。在新增客戶端埋點時最好是先梳理再分主路徑和重點 case 分別打點,此外還要對若干異常 case 的資料進行剔除或分類避免汙染打點資料,比如抖音在新增啟動時間打點時就會對開屏廣告、保活程序、push 拉起、deeplink 拉起、啟動期間退後臺、新使用者啟動等場景進行過濾或分開統計。

啟動優化的實踐

在做完理論分析與現狀分析後,我們基本對全域性待優化點及其大致優化方向會產生整體的認知,在開始落地各個優化措施之前還有很重要但往往會被忽略的一步——按優先順序排布優化項、制定整體優化方案,這一步在很大程度上制約著後續啟動優化的收益預期與進展把控,這兩點對於按時達成啟動優化的終極目標都至關重要。前述中提及了對“優先順序”的把控,這點是制定整體優化方案的重中之重。

從抖音啟動優化實踐總結來看比較好的優先順序策略是按照“投入產出比”來排布優化項,顧名思義:投入人力越少但優化幅度越大的優化項越應該排在前期,因為所有的效能優化歷程都勢必會經歷從高收益到低收益的變化,那麼相應的在排布優化項的前後順序時也需順應此規律,最終呈現的態勢即為:前期以小成本快速降低大盤啟動耗時,後期逐步提高投入突破各個瓶頸型耗時點(更後期大規模重構僅能減少幾十毫秒啟動時間的情形也應在預期之內),全過程同期加強防劣化機制,最終做到可持續優化。

在完成前述的全域性優先順序排布及方案制定後,才算真正來到了實施優化的階段,在這個階段所要用到的各類優化策略及配合方法在前文方法論部分已有詳細講述,在實戰部分首先要補充一下前述幾類優化策略按照“效能無損”、“業務無損”的區別劃分,整體如上圖所示,此外,我們會結合抖音啟動優化實戰經驗列舉各優化策略下可實施的優化項,以供參考:

  • 正面優化:刪減非必要的啟動邏輯、開屏頁與首頁 Activity 合併、獲取程序名從 IPC 轉反射方式等;
  • 按需優化:ContentProvider 中過早初始化邏輯轉為使用時初始化、多程序由啟動時載入轉為使用時或特定場景觸發載入等;
  • 延遲優化:4.x 機型延遲執行 Multidex.install 中的 Odex 操作、主執行緒訊息佇列中非啟動必要訊息延遲執行、啟動路徑非高優業務邏輯延遲初始化等;
  • 執行時優化:CPU 提頻、語言層面優化(內聯、替換反射、避免用 Kotlin 的 Range 迴圈)、關閉 Verify Class、4.x 機型抑制 GC、主動觸發 AOT 編譯、資源重排、類重排、dex 重排等;
  • 非同步優化:非同步預載入(ShardPreference、例項化物件)、非同步 inflate view、執行緒收斂等;
  • 降級優化:極速版、元件化降級、非必要耗時邏輯按人群/地區降級等;
  • 綜合優化:啟動任務排程框架、啟動路徑重構、前後臺啟動任務精細化重排、後臺負載優化等,這些優化項屬於前述優化思想的綜合應用,一般不侷限於單方面的優化。

通過上述列舉的各策略優化項你可能會發現,這其中有的優化項其實會對個別業務效能或功能有損,但最終對於啟動效能是有顯著提升的,那麼此時需要按照“全域性收益最大”的策略來綜合評估這些優化項的可落地性,並不是只看單點的得失,這種全域性性的思維在效能優化中非常重要。

線上驗證的實踐

這部分在前述的方法論中已針對三個關鍵點闡述得比較細緻,這裡僅針對三個關鍵點在落地時的技巧或注意事項加以補充:

  • 線下的優化一定要有線上的指標反饋:由於線上裝置的固有硬體效能各異,所以需要有足夠量級的使用者啟動打點資料才能相對準確地判定線下的優化是否在線上產生了效果,這個量級從抖音啟動優化中摸索的經驗來看一般達到 100 萬即可;此外,觀測啟動效能資料的時間點也需要把控好,這是由於每次釋出升級版 APP 後,大都是效能相對好的手機會先升級,這個現象會等導致發版初期的啟動效能資料整體偏好,不能反映真實大盤情形,因此,抖音一般會選取每個版本發版後 4-5 天(可能隨 APP 升級覆蓋安裝的速度不同而不同)的資料判定大盤情形。
  • 線上指標需要結合均值與分位值綜合來評估:在抖音啟動優化實踐中,啟動耗時均值會更多用於大盤情形評估或線上監控中,而作為效能優化的同學最主要關注的是 50 分位機型的資料,這是能代表過半數使用者啟動效能水準的指標,此外 90 分位以上的機型也需要我們額外關注,這類機型非常容易放大啟動效能問題,從實際來看,90 以上相對 50 的絕對分位數差了不到一倍,但冷啟耗時卻可能差到 2 倍左右(如下圖所示抖音在某段時期的各分位冷啟耗時情形),這說明低端機的使用者啟動體驗是明顯可感知的差,基於我們曾經做過的劣化實驗結果來看,這些機型的啟動效能如果不能有效提升,將有很大概率減少其留存。
  • 在驗證收益時通過 AB 實驗達成:AB 實驗相對於觀測不同版本的大盤資料來看更具有嚴謹性,因此在產出實驗結論前同樣需要保障資料量和時間跨度,抖音在開啟效能的 AB 實驗後,一般會讓對照組及實驗組進組使用者各達到 100 萬並保持至少 5 天后才進行實驗的資料分析併產出結論,這樣可以基本保障所有相關指標的穩定及置信。

防劣化的實踐

防劣化的體系建設是個比較複雜的工程,要做好是有非常大的挑戰的。抖音從最早的線下手動的分版本測試開始,經過了逐步的摸索優化,演變到當前涵蓋了程式碼提交時靜態檢測、線下自動化劣化測試和歸因、灰度劣化發現和歸因、線上常態化的劣化監控和歸因。防劣化是一個漏斗,從程式碼提交階段到線下測試階段,再到灰度釋出階段,再到線上版本釋出階段,我們希望劣化能夠更前置的發現,每個環節都儘可能的發現解決更多的劣化,保證更少的劣化被帶到線上。

防劣化的有幾個難點:一是劣化檢測的準確率和召回率,為了更多更準確的發現劣化;二是劣化的準確歸因,發現劣化之後,如果不能精準的指出劣化的原因,需要投入比較多的人力資源和時間定位劣化原因,影響劣化解決的效率;三是劣化的修復,如果是比較嚴重的劣化,可以採用阻塞發版限期解決的方式,是比較容易推進解決的。但是從抖音的實踐來看,當啟動優化到了深水區之後,優化的速度已經比較緩慢,需要關注幾十毫秒級別的劣化了,假設我們解決了一二兩個難點,發現了這些輕微的劣化,但是如何推進業務去解決這些小劣化也同樣是一個難題。我們需要能夠量化出這些劣化對業務的影響,針對不同的劣化量級,和業務對齊優先順序,確定標準的劣化修復流程,才能夠保證劣化不會被帶到線上影響大盤使用者。

防劣化是一個長期的工作,抖音投入已經有一年多了,目前整體效果還不錯,在這個過程中也積累了比較多的經驗,之後會專門寫一個抖音的防劣化系列文章來給大家介紹我們的技術成果。

啟動優化工具

古人云“工欲善其事必先利其器”,在啟動效能優化領域也是一樣,我們不僅需要趁手的工具來定位優化耗時問題,還需要儘量自動化的工具來持續發現劣化問題,也就是說整個啟動優化在“攻”和“守”的兩大方面均需要工具的輔助。那麼下面將針對這兩部分的工具分別進行介紹及分享抖音在啟動優化工具方面的探索:

線下分析工具

這部分主要針對業界常見的 APP 效能探測工具進行基本原理解析及優缺點對比,具體包含的工具有:TraceView、CPU Profiler、Systrace,此外還將提及抖音自研的“新一代全能型效能分析工具 RheaTrace”:

  • TraceView:Instrumentation 模式下采用 AddListener 的方式註冊 MethodError、MethodExited、MethodUnwind 的回撥來採集方法起止時間;Sampling 模式下使用一個 SamplingThread 定時主權執行緒堆疊,通過對此的堆疊對比近似確定函式的進入和退出時間;雖然是官方提供的工具,但兩種模式本身都存在比較大的效能損耗,可能帶偏優化方向;

  • CPU Profiler:整體通過 JVM Agent 實現,具有完成方法呼叫棧輸出,且支援 Java、C/C++方法的耗時檢測,上手比較簡單,但其同樣存在效能損耗較大的問題,且一般僅用於 debug 包,release 包需要額外新增 debuggable 的配置;

  • Systrace:基於 Android 系統層的 Atrace 實現,Atrace 又基於 Linux kernal 層的 ftrace 實現,ftrace 在核心中通過函式插樁獲取耗時;其自身效能損耗比較低、資料來源豐富且具有較好的視覺化頁面,但其預設監控點較少,在 APP 自有程式碼中的監控點需要手動加入,比較麻煩;

  • RheaTrace:這是抖音基於位元組碼插樁結合 Systrace 及 Atrace 自研的工具,其具有自動加入監控點、各類耗時資訊全面、效能損耗低等特點,是抖音日常線上下實施效能優化時首選的工具,其細節詳見前述公眾號文章,這裡不再贅述。

RheaTrace 目前是抖音效能優化同學的主要工具,它不僅僅是一個工具,也是一個平臺。除了 Systrace 自帶的效能資料之外,我們增加了業務的函式耗時插樁的資料,可以更全面地對耗時進行分析。但是這些資料還不夠,我們支援以外掛的形式,增加自己定製的資料,比如為了優化 IO 的耗時,我們通過 hook 增加了更精細化的 IO 的資訊,輔助定位 IO 的耗時問題;抖音的類載入耗時也是有些嚴重,我們也 hook 了類載入,增加了類載入的效能資料。我們要極致地優化抖音啟動時間,以上這些資料是不夠的,還有鎖、View 耗時資訊等相關資料補充,給效能優化的同學提供全方位的效能分析工具。

除了 RheaTrace 之外,還有一些特定場景的小工具,比如執行緒分析工具、記憶體分析工具、高頻函式分析等。由於篇幅有限,就不在這裡一一介紹,後面會有專門的系列文章來介紹。

線上監控工具

上面介紹啟動優化方法論的時候我們提到了,不能只是看線下的效能分析,線下的分析結果並不能完全代表線上大盤使用者的情況。我們分析線上的效能資料,一方面能夠驗證我們的線上優化效果,另一方面能夠從線上多個維度的資料裡指導後續的優化方向。

線上監控工具和線下的差異點主要在低效能損耗和相容性,我們將 RheaTrace 做了改造,使其能夠滿足線上的監控要求。效能損耗上,我們將監控的效能損耗控制在 1%以內,包大小控制在 200KB 以內,基本實現了線上全量使用者的啟動耗時監控。通過啟動路徑的全量插樁,可以針對啟動路徑的各個階段進行監控,一是可以發現線上使用者哪些任務比較耗時,可以針對性的優化,讓更多使用者受益;二是可以監控線上的啟動任務,如果發生了耗時增加,那麼說明有劣化,這比監控到啟動時間的劣化,要更容易定位到原因。除了線上的全量慢函式監控之外,我們的線上啟動監控還會細化IO、鎖、GC等多種維度的耗時資料,幫助定位線上為什麼耗時慢,提供新的優化方向。

總結一下線上啟動監控工具的思路就是:將線下的效能分析資料,低損耗的移植到線上,觀察線上使用者的效能資料,線上線下相結合的分析啟動耗時,為啟動優化提供優化方向指導。

啟動效能優化之路去向何方

看了上文關於啟動效能優化如此多的理論與實踐,想必你已經意識到啟動優化之路註定是不會平凡的,抖音在這條路上探索了 2 年之久且仍未到達盡頭。在這條路上勢必會經歷前期的坦途、中期的迷茫與後期的瓶頸,但無論如何都要一直堅定地走下去,因為只要業務還有一天在迭代那麼啟動效能就有一天存在挑戰的可能,所以啟動優化之路的未來必然是無盡頭的

既然如此,那麼我們的重點就應該從何時才能走完這條路轉移到如何走得更精彩之上,甚至到最後能夠做到把控這條路的走向,這或許也能算作另一種意義上的走完啟動優化之路,那麼什麼才算走得更精彩以及把控路的走向呢?

迷茫時慢下步子再分析全域性的耗時點尋找到新的優化策略、遇到瓶頸時先暫時放緩追趕指標嘗試從程式碼重構上挖掘深層的收益、不斷開拓跨領域(如端上智慧降級)結合的優化方向……這些或許都能稱作是一種精彩,並且會因人而異,最終,當這種精彩累計得足夠多之時我們很可能會發現啟動優化之路上已知的所有岔路口全被走了個遍,同期 APP 的啟動效能也很可能已經達到了再優化也沒什麼明顯業務收益的地步,並且出現的任何劣化點都能及時被解決掉,那麼這時不出意外的話,啟動優化之路走向的把控權已經盡在你手中了。