一. 前言
啟動性能是百度App最核心名額之一。使用者希望應用能夠及時響應并快速加載,啟動時間過長的應用不能滿足這個期望,并且可能會令使用者失望,這種糟糕的體驗可能會導緻使用者在應用商店針對您的應用給出很低的評分,甚至完全抛棄您的應用。啟動性能的優化成為了體驗優化中最關鍵的一環,百度App在此方向持續投入,不斷優化,提升使用者體驗。
啟動性能優化分為概述篇、工具篇、優化篇和防劣化篇,本篇文章主要闡述性能優化相關内容,前期已發表文章可以參閱:
百度App 低端機優化-啟動性能優化(概述篇):
https://mp.weixin.qq.com/s/aomafRByyponPBiI79OoHQ
百度App Android啟動性能優化-工具篇:
https://mp.weixin.qq.com/s/kyTjEQutXQ7oqzN0htTOiA
百度App性能優化工具篇-Thor原理及實踐:
https://mp.weixin.qq.com/s/kKRNpQ0UNP2m0_vOS6D8HA
二. 優化理論
對啟動性能優化的認知,決定了啟動性能優化的方向與思路,進而會決定優化的效果。較多開發者對啟動過程的認知,來源于Google 開發者文檔中有段對啟動過程的描述:
- 建立應用對象;
- 啟動主線程;
- 建立主 activity;
- 擴充視圖;
- 布局螢幕;
- 執行初始繪制。
一旦應用程序完成第一次繪制,系統程序就會換掉目前顯示的背景視窗,替換為主 activity。此時,使用者可以開始使用應用。
上面主要介紹了應用在啟動過程中的各個階段,但其實隻是大緻概括,其實啟動方式會比較多,極有可能在不同的啟動路徑執行的邏輯有差異,是以全路徑的認知在優化過程中起到了非常重要的作用,如下圖所示:
在啟動過程中,點選桌面圖示是主流冷啟動方式,而Push調起,浏覽器調起等端外轉化也是比較常見的調起方式,各種啟動方式的啟動過程基本可拆解為:程序建立、架構加載、首頁渲染、預加載四個環節。而啟動性能優化主要面對的不隻是點選桌面圖示這一種路徑,更多的需要啟動全路徑的優化,達到體驗的極緻優化。
啟動過程也需要結合系統層面來了解,進而挖掘優化點,探索優化的極限。啟動過程是非常複雜的過程,需要較多系統級程序配合才能完成頁面的展現,供使用者正常使用,下圖展示的點選icon的啟動過程:
啟動過程大概可概括為:
- Launcher通知AMS啟動APP的主Activity;
- ActivityManagerService(以下簡稱AMS)記錄要啟動的Activity資訊,并且通知Launcher進入pause狀态;
- Launcher進入pause狀态後,通知AMS已經paused了,開始啟動App;
- App未開啟過,AMS啟動新的程序,并且在新程序中建立ActivityThread對象,執行其中的main函數方法;
- App主線程啟動完畢後通知AMS,并傳入applicationThread以便通訊;
- AMS通知App綁定Application并啟動MainActivity;
- 啟動MainActivitiy,并且建立和關聯Context,最後調用onCreate方法,最終完成頁面繪制和上屏;
主要程序的功能主要是:
- Launcher程序:為手機桌面程序,負責接收使用者的點選事件,并将事件通知到AMS
- SystemServer程序:負責應用的啟動流程排程、程序的建立和管理、視窗的建立和管理(StartingWindow 和 AppWindow) 等,比較核心的服務有AMS和WMS(WindowManagerService);
- Zygote程序:通過fork建立應用程式程序,Zygote程序在初始化時就會會建立虛拟機,同時把需要的系統類庫和資源檔案加載到記憶體中。而Zygote在fork出子程序後,這個子程序也會得到一個已經加載好基礎資源的虛拟機,進而加速應用程序的啟動;
- SurfaceFlinger程序:主要和應用的渲染相關,如Vsync信号處理、視窗的合成處理、幀緩沖區管理等。
有了全局的認知和視野後,我們就可以站在更高的角度,更加深入的思考與分析性能瓶頸,如手機負載合理性、系統資源使用等等,更加全面的考慮啟動性能的優化方式,達到對啟動性能的極緻優化。
三. 優化落地
百度App的啟動性能的優化,大緻分為三部分,正常優化、基礎機制優化和底層技術優化三部分。
丨3.1正常優化
如果是業務發展初期,業務的快速疊代較快,此時的優化會相對簡單,極有可能會出現短時間内,啟動速度提升秒級别的優化效果。啟動性能的優化,也是基于對冷啟動的了解以及啟動任務的梳理,達到快速優化的目标。可通過性能工具,如前文提過的Trace工具、Thor Hook工具,發現耗時較為突出問題,評估是否可通過延遲、異步、删除等方式優化,依據投入産出情況評估工作優先級,達到快速優化啟動性能的目的。
随着啟動場景承載業務逐漸龐大,手百逐漸成長為承載業務最多,體量巨大的航母級移動端應用,龐大業務的預加載不可能完全去除或者通過異步來解決,此部分是啟動性能優化中面臨的較大難題,需要有機制批量解決業務預加載問題,是以基礎機制中的排程機制逐漸衍生出來,處理啟動過程不同業務的預加載需求。
丨3.2基礎機制優化
基礎機制優化主要分為排程機制優化、基礎元件性能優化。
丨3.2.1 任務排程優化
業務多,預加載任務的執行訴求各有不同,平衡啟動性能和業務預加載,百度App需建設任務排程架構,業務方通過接入可快速優化性能問題。
任務排程整體建設情況如下,目前還處在快速疊代中:
智能排程可以根據任務輸入和資訊輸入,做出不同的排程反應,如:
- 個性化排程政策:識别出業務預加載任務ID和使用者行為習慣相比對,則會将任務提前做初始化,任務優先級會做提升,與此同時,在使用者進入業務對應頁面時,非業務相關任務需做避讓;
- 分級體驗政策:識别出在指定的機型配置中有對應的排程政策,則會執行對應的排程能力,如立即排程、延遲排程、不排程等,主要用于體驗降級;
- 精細化排程政策:在不同的場景精細化排程業務預加載任務,如在閃屏場景,會識别閃屏相關業務資訊并做預加載,在端外調起場景,會識别落地頁所屬業務資訊并做對應預加載;
- 分優先級延遲排程:有較大量的任務初始化會依賴于延遲排程,需保障有序控制業務初始化,是以在延遲排程基礎上添加優先級概念,可以在延遲排程中也分優先級排程,讓更高優先級任務可以更快的執行;
- 首頁UI并行渲染排程:主要服務于冷啟動階段商業閃屏業務,商業閃屏是否需要展現、展現哪個物料均是冷啟動階段的實時網絡請求決定的,需在冷啟動階段盡量提高商業網絡請求的可用時間,進而提高網絡請求成功率,百度App目前可以實作,首頁可以先初始化,但不做上屏,待首頁渲染業務送出的時候再檢查商業閃屏是否展現,做到了提供給商業網絡請求更多可用時間的同時不阻塞首頁初始化,通過此項技術大幅提升商業網絡請求的成功率,帶來了商業收入的提升。
由于排程器架構中涉及細節非常多,在這裡隻簡單介紹其中一種排程器的設計:分級體驗排程器。
主要分為3個子產品,機型評分、分級配置和分級排程機制,達到不同配置的手機上的最優體驗。
- 機型評分:
- 通過裝置資訊計算評分資訊,稱為靜态評分;
- 通過性能名額計算評分資訊,稱為動态評分;
- 依據模型訓練評分資訊,得出最終機型評分;
- 分級配置:
- 雲端配置表:提供各業務級别按裝置評分條件下的分級配置表,該表支援動态更新,增量更新,更新後端上及時生效。
- 本地預置表:本地會預置一份配置表,供首次安裝時使用;
- 依據機型評分資訊和分級配置資訊得出控制政策;
- 分級排程:
- 業務方根據機型評分控制不同的業務邏輯,達到高端機全部功能最優體驗,中端機部分功能良好體驗,低端機核心功能流暢體驗,如首頁點贊動畫在高端機上選擇開啟政策,中端機上選擇延遲加載政策,低端機上選擇關閉狀态;
丨3.2.2 KV存儲優化
SharedPreferences是Android平台輕量級的存儲類,用來儲存應用程式的配置資訊,其本質是以“鍵-值”對的方式儲存資料的xml檔案,其檔案儲存在/data/data/pkg/shared_prefs目錄下,優點是以鍵值對的方式進行存儲,使用友善,易于了解;但SharedPreferences的缺點很明顯,讀寫性能慢,IO讀寫使用xml資料格式,全量更新效率低;多程序支援差,存儲資料易丢失;建立線程多,導緻性能差。
讀取性能差
每加載一個SP檔案均會建立子線程,源碼如下:
private final Object mLock = new Object();
private boolean mLoaded = false;
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
但是在擷取key-value時如果沒有加載完成,則會wait等待SP檔案加載完成:
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
寫入性能差
SP采用XML格式,每次寫入是全量更新,效率低,寫入提供兩種方式:
- commit:阻塞目前線程方式,修改送出到記憶體後,等待IO完成,如果主線程使用commit方式,極有可能出現卡頓;
- apply:不阻塞目前線程,但也有隐藏的坑,可能會導緻主線程的卡頓問題,主要原因為apply方式将寫入Runnable加入到QueueWork中,而在Android 四大元件生命周期輪轉時,會檢查QueueWork是否完成,如果沒有完成則會wait,代碼如:
public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
......
// 確定寫任務都已經完成
QueuedWork.waitToFinish();
......
}
}
是以,在ANR/卡頓監控中能看到非常多的SharedPreferences堆棧,看堆棧是系統級堆棧,但其實是SP apply方式引入的問題,堆棧如:
java.lang.Object.wait(Native Method)
java.lang.Thread.parkFor$(Thread.java: )
sun.misc.Unsafe.park(Unsafe.java: )
java.util.concurrent.locks.LockSupport.park(LockSupport.java: )
java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java: )
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java: )
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java: )
java.util.concurrent.CountDownLatch.await(CountDownLatch.java: )
android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java: )
android.app.QueuedWork.waitToFinish(QueuedWork.java: )
android.app.ActivityThread.handleServiceArgs(ActivityThread.java: )
android.app.ActivityThread. - wrap21(ActivityThread.java)
android.app.ActivityThread$H.handleMessage(ActivityThread.java: )
android.os.Handler.dispatchMessage(Handler.java: )
ndroid.os.Looper.loop(Looper.java: )
ndroid.app.ActivityThread.main(ActivityThread.java: )
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java: )
com.android.internal.os.ZygoteInit.main(ZygoteInit.java: )
多程序支援差
當使用MODE_MULTI_PROCESS這個字段時,其實并不可靠,因為Android内部并沒有合适的機制去防止多個程序所造成的沖突,應用不應該使用它,推薦使用ContentProvider。上面這段介紹我們得知:多個程序通路{MODE_MULTI_PROCESS}辨別的SharedPreferences時,會造成沖突,舉個例子就是,在A程序,明明set了一個key進去,跳到B程序去取,卻提示null的錯誤。
3.2.2.1 優化方案設計
目前各大廠商也對SP做了一定優化,有保守優化,在SP目前機制基礎上做優化,主要是解決寫入導緻的ANR問題;也有颠覆性優化,比較有代表性的為MMKV和Data Store,但經評估後,可能均有一定問題,是以在百度App的優化中,也是學習借鑒業界主流的處理方式,最終采用兩種優化并存的方式:
- 提供颠覆性優化元件:UniKV,徹底解決原生SP一系列問題,核心場景極緻體驗,業務方主動接入;
- 在系統SP機制上做優化,解決寫入時ANR等痛點問題,主要服務于未接入UniKV的SP檔案;
3.2.2.1.1 UniKV設計
層級設計
1: 業務使用時直接依賴UniKV,UniKV繼承SharedPreferences,對齊原生SP接口;
2: 工程中包含原生實作和UniKV實作,代碼中直接依賴原生實作,編譯打包時替換為UniKV實作,保證業務中台輸出能力;
檔案存儲格式設計
分位檔案頭、資料塊。檔案頭40個位元組,主要存儲版本号、回寫次數、保留字段、容災資料長度、容災CRC、實際資料長度、實際CRC。
1:以4KB位機關配置設定空間,最小占用4KB空間,通過mmap映射檔案,作業系統負責資料寫入檔案;
2:通過容災資料長度和容災CRC可做資料恢複;
3:通過保留字段可做功能拓展,比如是否從SP遷移成功辨別;
資料塊中存儲主要資料體,以append形式寫入,必要時再做資料整理
1:支援類型存儲,對齊SP原生getAll接口;
2:支援類型有:BOOL、INT、FLOAT、DOUBLE、SHORT、LONG、STRING、STRING_ARRAY、BYTE_ARRAY9種類型,相比于原生SP實作支援類型更多;
資料遷移
資料遷移過程需要先讀取SP内容,再寫入KV檔案,耗時會較久,寫入完成後KV檔案才可用,這線上上會有隐患,需要解決。
UniKV中資料遷移采用不影響業務方式,如果遷移完成,則會直接使用KV檔案,如果未遷移完成,則繼續使用SP檔案,并将資料遷移Runnable送出至線程池。為避免資料遷移期間SP檔案出現改動導緻資料丢失,注冊SP檔案更改的資料監聽。遷移完成标記位由保留字段來存儲,往往資料遷移時需要标記位來儲存是否遷移完成的Flag,需要引入其他檔案來儲存,此處UniKV裡的保留字段很好的解決了此問題。
多程序實作
采用mmap機制 + 自定義檔案鎖實作程序間資料同步,mmap檔案至每個程序的記憶體空間,自定義檔案鎖主要實作的遞歸鎖和鎖的升降級,多程序讀時共享鎖,多程序寫時排他鎖,原生檔案鎖不支援遞歸鎖,升降級容易死鎖或鎖會被完全釋放,是以自定義檔案鎖實作程序間資料同步。關于多程序這塊實作,主要學習了MMKV的多程序實作邏輯,感興趣的可以參閱:https://github.com/Tencent/MMKV/wiki/android_ipc
實作效果
徹底解決原生SP的性能問題,讀寫性能顯著提高,支援多程序讀寫,減少線程建立,整體性能名額和業務名額均出現了明顯優化。
3.2.2.1.2 系統SP機制優化
有些SP是在插件、第三方SDK中使用的,是以無法使用UniKV統一優化,需提供一種優化原生SP機制的方案。
優化方案:
Android版本 | ANR類型 | ANR原因 | 性能平台ANR資料 | 優化思路 |
Android<8.0 | writenToDiskLatch.wait | writenToDiskLatch.wait等待子線程寫入完成 | Top 3,ANR占比:4.7% | 動态代理QueueWork的sPendingWorkFinishers,使poll傳回null |
Android>= 8.0 | writenToDiskLatch.wait | writenToDiskLatch.wait等待子線程寫入完成 | Top 15,ANR占比:0.5% | 動态代理QueueWork的sFinishers,使poll傳回null |
processPendingWork | main線程已獲得sProcessLock鎖,在main正在執行寫磁盤任務writeToFile,但耗時過大 | Top 11,ANR占比:0.8% | 代理sWork的clone函數都傳回空隊列;通過反射擷取QueuedWork的mHandler的Looper對象,建立一個新的Hander,并将sWork中的任務送出到這個Handler去執行,進而實作了無阻塞運作 | |
processPendingWork | main線程等在了擷取 sProcessLock鎖,子線程獲得了sProcessLock鎖在執行寫磁盤任務,main等待時間過長 | Top 7,ANR占比:1.8% | 代理sWork的clone函數都傳回空隊列;通過反射擷取QueuedWork的mHandler的Looper對象,建立一個新的Hander,并将sWork中的任務送出到這個Handler去執行,進而實作了無阻塞運作 |
目前百度App 在Android 12上暫未優化,主要原因是Android 12實作方式有變化,代理方式相對複雜,且開銷較大,而SP引起的ANR問題較少,是以暫未上線優化。
優化效果:
此方案對全局均有優化,除了ANR名額有顯著下降外,DAU和留存也出現正向。有同學會擔心優化後資料寫入是否會受影響,我們通過打點監控到SP寫入及時性沒有明顯變化,而寫入成功率出了正向,低端機提升明顯,說明SP優化減少ANR的發生,更多任務被執行,寫入成功率提升。
丨3.2.3 鎖優化
多線程性能調優是性能優化中不可避免的話題,為了實作線程同步,加入了同步鎖機制(Synchronized同步鎖、Lock同步鎖等),同步鎖的誕生雖然保證了操作的原子性、線程的安全性,但是(相比不加鎖的情況下)造成了程式性能下降。是以,我們這裡要做的一件事就是“鎖優化”,即既要保證實作鎖的功能(即保證多線程下操作安全)又要提高程式性能(即不要讓程式因為安全而損失太大效率)。
常見的鎖優化方式:
下面以一個優化項,介紹百度App在鎖優化中的實際優化落地。
在項目開展初期,通過Trace工具分析發現有較多的“monitor contentation XXX”,此部分資訊是Android ART虛拟機輸出的鎖相關資訊,其中會包括持有鎖的線程、方法、等鎖線程、等鎖方法。具體如下圖所示:
經分析,主要是基礎元件的AB在初始化時由于synchronized關鍵字不正确使用導緻,需對AB做性能優化,必要時做架構更新。而經過分析,AB基礎元件在多線程、檔案IO性能均存在性能問題,是以對AB基礎元件做了重構更新,徹底解決性能問題。
通過優化後,讀寫采用無鎖實作,徹底解決業務使用ABTest元件鎖同步問題;相容新老AB資料,緩存實驗開關和實驗sid資料,并采用JSON/PB資料格式存儲,首次讀取性能118ms,優化95%(小米5機器)。
丨3.2.4 其他基礎機制優化
在百度App的啟動性能優化中,開展過較多的基礎機制相關優化,如:線程優化、IO優化、SO優化、主線程優先級優化、ContentProvider優化、類/圖檔預加載優化、圖檔預上傳GPU優化等。
線程優化
通過Hook能力編寫插件,發現線程使用不規範問題,制定線程使用規範,如:
1: 業務禁止私自設定線程優先級;
2: 提供統一的線程池,避免各業務各一個線程池;
3:優先選擇線程池/任務排程器排程,業務禁止單獨建立線程/線程池;
4: 線程池需避免線程頻繁建立,參數标準化。
IO優化
通過Hook能力編寫插件,發現不合理IO問題,主要包括:
- 主線程讀寫時間超過100ms,主線程讀寫時間過長會導緻主線程長耗時問題,嚴重時可能會導緻ANR問題;
- 讀寫buffer過小問題,如果buffer太小,會導緻過多次系統調用和記憶體拷貝,read/wirte次數過多,進而影響性能。
SO優化
通過Hook能力編寫插件,發現So加載問題,優化不必要的SO加載過程,對于必要的加載,嘗試通過異步線程提前政策解決,達到優化性能的目的。
Binder優化
通過Hook能力編寫插件,發現Binder通信相關問題,優化不必要Binder通信,必要時可通過記憶體緩存、檔案持久化等方式,達到優化性能的目的。
主線程優先級
主線程的優先級決定了系統為主線程配置設定的資源,如果線程優先級有問題,被改成了低優先級,極有可能出現得不到CPU時間片導緻運作慢的問題。在主線程優先級的問題排查中,最有代表性的是業務在為相關子線程設定優先級時誤設定了優先級,出問題方式:
Thread t = new Thread();
t.start();
t.setPriority(3);
Android的離奇陷阱—設定線程優先級導緻的微信卡頓慘案:https://mp.weixin.qq.com/s/oLz_F7zhUN6-b-KaI8CMRw
在百度App排查優先級設定時,原生庫也有更改線程優先級邏輯,也需主動修正,如facebook react庫的部分邏輯:
ContentProvider/FileProvider優化
在Application.attachbaseContext和Application.onCreate之間,會執行installContentProviders方法,在此方法中會執行AndroidManifest中聲明的ContentProvider/FileProvider,一般耗時較大的為FileProvider,主要原因是FileProvider初始化時有IO操作。主要優化為将ContentProvder/FileProvider移除,并通過android:process屬性做控制,或者通過懶加載方式,必要程序中初始化。
圖檔prepareToDraw優化
在Trace工具有會看到RenderThread中執行syncFrameState時會upload XXX Texture相關耗時問題,首先檢查在trace裡面顯示的圖檔的寬和高,確定圖檔的大小不比它顯示出來的區域大太多。也可以通過prepareToDraw方法提前觸發Bitmap上傳GPU操作,這種方式可以使Bitmap在RenderThread空閑的時候提前完成。理想情況下,圖檔加載庫會幫助你完成這些;如果你想要自己掌控圖檔加載,或者需要確定不在繪制的時候觸發Bitmap上傳,可以直接在代碼裡面調用 prepareToDraw。
可能有的同學比較疑惑,此優化沒有優化主線程,會對啟動性能有優化嗎?答案是可以優化主線程,在啟動的前幾幀,每一幀耗時均會比較大,而每一幀的任務在RenderThread中以DrawFrame Task運作,如果上一幀的任務沒有完成,則會阻塞目前幀的繪制,主線程中展現出來的就是draw過程變慢,如nSyncAndDrawFrame執行時長過長。
丨2.3底層機制優化
主要通過探索底層的技術,來實作優化性能名額,進而撬動業務價值的目标,此方向風險性相對較高,成本也會較大,需依據具體人力情況及優化效果做最終決策。
百度App中目前已嘗試過的有VerifyClass優化、CPU Booster優化、GC相關優化等,目前還在探索一些技術點,此部分優化基本為全局優化,會在後續的流暢度專題中為大家揭曉。
小結
啟動性能優化是相對複雜的技術方向,不僅有較多的業務會和啟動性能有千絲萬縷的聯系,在啟動過程中也有非常多的系統行為值得關注與投入,目前百度App啟動性能已逐漸步入瓶頸期,如何打破瓶頸并與業務緊密結合,是啟動性能優化的挑戰與機遇。啟動性能的優化是不斷學習、不斷颠覆、不斷進步的過程,中間可能會遇到非常多的挑戰,也會出現非常多的機遇,是以,啟動性能優化永無止境,任重而道遠。
參考資料:
1、抖音啟動優化
https://heapdump.cn/article/3624814
2、快手TTI 治理經驗分享
https://zhuanlan.zhihu.com/p/422859543
3、淺析Android啟動優化
https://juejin.cn/post/7183144743411384375
4、MMKV:
https://github.com/Tencent/MMKV/wiki/android_ipc
作者:龍少
來源:微信公衆号:百度App技術
出處:https://mp.weixin.qq.com/s/Q6Z3pQpYWtQ_X9bk-KJdzw