基準設定檔

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

基準設定檔是一份由不同類別和方法組成的清單。當 Android 執行階段 (ART) 進行安裝時,會使用這些包含在 APK 中的類別和方法,將重要路徑預先編入機器碼。這個設定檔是檔案導引最佳化 (PGO) 的一種形式,能讓應用程式最佳化啟動作業、減少卡頓,及改善終端使用效能。

基準設定檔的運作方式

系統會將檔案規則轉換為 APK 中的二進位格式,位於 assets/dexopt/baseline.prof

在安裝期間,ART 會預先 (AOT) 編譯設定檔中的方法,加快它們的執行速度。如果設定檔中包含應用程式啟動或進行畫面轉譯時使用的方法,使用者將能感受到程式啟動變快且/或卡頓狀況減少。

開發應用程式或程式庫時,我們建議規範基準設定檔以涵蓋關鍵使用者旅程中的特定熱門路徑。因為對於這些路徑,例如程式啟動、系統轉換和頁面滾動,轉譯時間和延遲時間長短是很重要的。

然後就能將基準設定檔與 APK 一併提供給使用者 (透過 Google Play)。

這張圖表展示從上傳資料到最終發布之間的基準設定檔工作流程,以及該工作流程與雲端設定檔之間的關係。

使用基準設定檔的原因

應用程式的啟動時間是提升使用者參與度的關注重點。提升啟動效率和回應速度除了會增加每日活躍用戶數,還能夠提高平均回訪率。

雲端設定檔也會最佳化上述提到的互動功能,但至少要在發布更新後的一天過後才能體驗到最佳化效果。另外,這項服務不支援 Android 7 (API 24) 至 Android 8 (API 26) 版本。

Android 各版本的程式行為

Android 平台中的各版本採用不同的應用程式編譯方式,各有各的優缺點。基準設定檔可為所有已安裝項目提供一份檔案,來改善先前的編譯方法。

Android 版本 編譯方法 最佳化方法
Android 5 (API 層級 21) 至 Android 6 (API 層級 23) 完整 AOT 整個應用程式在安裝作業期間都會進行最佳化,因此會造成長時間等候使用應用程式、RAM 和磁碟空間使用量增加,以及從磁碟載入程式碼的時間拉長等狀況,甚至冷啟動時間也可能會變長。
Android 7 (API 層級 24) 至 Android 8.1 (API 層級 27) 部分 AOT (基準設定檔) 第一次跑動應用程式時,基準設定檔會透過 androidx.profileinstaller 下載,程式模組也會規範這個依附元件。當裝置處於閒置狀態時,ART 有機會透過新增額外的檔案規則加上編譯,來進一步改善上述規範。這將會大幅改善磁碟空間及從磁碟載入程式碼的時間問題,因此縮短了應用程式等待時間。
Android 9 以上版本 (API 層級 28) 部分 AOT (基準 + 雲端設定檔) Google Play 會在應用程式安裝期間使用基準設定檔,來最佳化 APK 和雲端設定檔 (如果有的話)。安裝完畢後,ART 設定檔會上傳至 Google Play 並進行匯總,然後在其他使用者安裝/更新應用程式時,以雲端設定檔的形式提供。

建立基準設定檔

利用 BaselineProfileRule 自動建立設定檔規則

作為應用程式開發人員,您可以使用 Jetpack Macrobenchmark 程式庫,在每次應用程式發布時自動產生設定檔。

以下是如何用 Macrobenchmark 程式庫建立基準設定檔:

  1. 請在應用程式 build.gradle 中的 ProfileInstaller 程式庫中新增依附元件,以便啟用本地和 Play 商店基準設定檔編譯。這是在本地設備側載基準設定檔的唯一方法。

    dependencies {
         implementation("androidx.profileinstaller:profileinstaller:1.2.0-alpha01")
    }
    
  2. 在 Gradle 專案中設定 Macrobenchmark 模組

  3. 定義名為 BaselineProfileGenerator 的新測試,如下:

    @ExperimentalBaselineProfilesApi
    @RunWith(AndroidJUnit4::class)
    class BaselineProfileGenerator {
        @get:Rule val baselineProfileRule = BaselineProfileRule()
    
        @Test
        fun startup() =
            baselineProfileRule.collectBaselineProfile(packageName = "com.example.app") {
                pressHome()
                // This block defines the app's critical user journey. Here we are interested in
                // optimizing for app startup. But you can also navigate and scroll
                // through your most important UI.
                startActivityAndWait()
            }
    }
    
  4. 連接執行 Android 9 或以上版本的 userdebug 或已啟用 Root 權限的 Android 開放原始碼計畫 (AOSP) 模擬器。

  5. 執行 adb root 來取得 Root 權限。

  6. 執行測試,並在 logcat 中找出產生設定檔的位置。搜尋記錄標記 Benchmark

    com.example.app D/Benchmark: Usable output directory: /storage/emulated/0/Android/media/com.example.app
    
    # List the output baseline profile
    $ ls /storage/emulated/0/Android/media/com.example.app
    SampleStartupBenchmark_startup-baseline-prof.txt
    
  7. 在裝置上開啟產生的檔案。

    $ adb pull storage/emulated/0/Android/media/com.example.app/SampleStartupBenchmark_startup-baseline-prof.txt .
    
  8. 將產生的檔案重新命名為 baseline-prof.txt,並複製到應用程式模組的 src/main 目錄。

手動定義設定檔規則

您可以透過在 src/main 目錄中建立名為 baseline-prof.txt 的檔案,藉此手動定義應用程式或程式庫模組中的設定檔規則。上述資料夾與存有 AndroidManifest.xml 檔案的是同一個。

在這個檔案中,一行指定一個規則。每個規則都代表一種特定模式,其中每個模式都包含應用程式或程式庫中同樣需要最佳化的對應方法或類別。

使用 adb shell profman --dump-classes-and-methods 時,這些規則的語法算是一個人類可讀的 ART 設定檔格式 (HRF) 超集。該語法和描述元及簽名的語法非常類似,但前者同時也允許使用萬用字元來簡化規則寫入程序。

下列範例列出了包含在 Jeetpack Compose 程式庫中的幾個基準設定檔規則:

HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)V
HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
HLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()V
PLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
HLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
Landroidx/compose/runtime/ComposerImpl;

規則語法

這些規則能以兩種模式之一來指定方法或類別:

[FLAGS][CLASS_DESCRIPTOR]->[METHOD_SIGNATURE]

類別規則使用以下模式:

[CLASS_DESCRIPTOR]
語法 說明
FLAGS 代表 HS,和 P 的一或多個字元,點出此方法應標記為 HotStartup,或 Post Startup,與啟動類型相關。

帶有 H 標記表示這個方法很「熱門」,也就是會在應用程式生命週期間被多次呼叫。

帶有 S 標記表示這個方法會在應用程式啟動時被呼叫。

帶有 P 標記表示這個方法是與程式啟用無關的熱門方法。

存在於這個檔案中,代表這個類別會在程式啟動時被選用,應在堆積中預先分配以減少類別載入時的消耗。ART 編譯器採用多樣的最佳化策略,例如上述的 AOT 編譯方法,以及最佳化 先前產生的 AOT 檔案的版面配置。
CLASS_DESCRIPTOR 指定方法類別的技術子。舉例來說,androidx.compose.runtime.SlotTable 的記述子是 Landroidx/compose/runtime/SlotTable;。注意:系統會依據Dalvik Executable (DEX) 格式在開頭加上 L 字。
METHOD_SIGNATURE 方法的簽署,包括方法的名稱、參數類型和傳回類型。舉例來說,LayoutNode 之上的方法

// LayoutNode.kt

fun isPlaced():Boolean {
// ...
}

擁有 isPlaced()Z 簽署。

這些模式可利用萬用字元,以便讓單一規則包含多種方法或類別。如在使用 Android Studio 的規則語法時需要引導或協助,請參考Android 基準設定檔外掛程式。

萬用字元規則的一範例如下:

HSPLandroidx/compose/ui/layout/**->**(**)**

基準設定檔規則的支援類型

基準設定檔規則支援下列類型。想進一步瞭解這些類型,請參考式Dalvik Executable (DEX) 格式

字元 類型 說明
B 位元組 位元組 (已簽署)
C 字元 編入 UTF-16 的統一碼字元碼位
D 雙倍 雙精度浮點數
F 浮動 單精度浮點數
I int 整數
J long long int
S short signed short
V void void
Z 布林 對或錯
L (類別名稱) 參考資料 類別名稱的例子

此外,程式庫也可以規範即將封裝於 AAR 構件中的規則。當您建構一 APK 以納入這些構件時,系統會將規則合併在一起 (類似於資訊清單的合併方式),並編譯入 APK 專用的壓縮二進位 ART 設定檔。

當Android 9 (API 層級 28) 版本的裝置啟動,使用APK預先編譯程式的一特定子集時,ART的環境就能善用這個檔案。而對於Android 7 (API 層級 24) 版本則是在使用ProfileInstaller時,ART 會發揮效果。

附註

建立基準設定檔時,請注意以下額外幾點:

  • Android 5 到 Android 6 版 (API 層級 21 和 23) 會在安裝前預先編譯 APK。

  • 我們絕對不會預先編譯可偵錯的應用程式來協助排解問題。

  • 規則檔案必須命名為 baseline-prof.txt,並置於主要來源集的根目錄中 (應該要是 AndroidManifest.xml 檔的一個子檔案)。

  • 您需要使用 Android Gradle 外掛程式 7.1.0-alpha05 以上的版本 (Android Studio Bumblebee Canary 5) 才能利用這些檔案。

  • Bazel 目前尚不支援讀取基準設定檔,或將其合併成 APK。

  • 基準設定檔壓縮後不得超過 1.5 MB。因此程式庫和應用程式應盡力規範出擁有足夠功能,但較少的檔案規則。

  • 如果規則編得過於複雜,應用程式會因為磁碟存取量變大而降低啟動速度。我們在此建議您測試基準設定檔的效能。

評估改善空間

利用 Macrobenchmark 程式庫進行自動化評估

Macrobenchmark 可讓您透過CompilationMode API (包括 BaselineProfile 的使用) 來控制預先評估。

如果您已在 Macrobenchmark 模組中設定 BaselineProfileRule 測試,則可在該模組中定義一項新測試來評估效能:

@RunWith(AndroidJUnit4::class)
class BaselineProfileBenchmark {
  @get:Rule
  val benchmarkRule = MacrobenchmarkRule()

  @Test
  fun startupNoCompilation() {
    startup(CompilationMode.None())
  }

  @Test
  fun startupBaselineProfile() {
    startup(CompilationMode.Partial(
      baselineProfileMode = BaselineProfileMode.Require
    ))
  }

  private fun startup(compilationMode: CompilationMode) {
    benchmarkRule.measureRepeated(
      packageName = "com.example.app",
      metrics = listOf(StartupTimingMetric()),
      iterations = 10,
      startupMode = StartupMode.COLD,
      compilationMode = compilationMode
    ) { // this = MacrobenchmarkScope
        pressHome()
        startActivityAndWait()
    }
  }
}

傳遞測試結果的範例如下:

這是小型測試的結果。更大型的應用程式可以透過基準設定檔獲得更多優勢。

請注意,雖然上方範例關注的是 StartupTimingMetric,但仍有其他重要指標需要評估,例如可以使用 Jetpack Macrobenchmark 進行衡量的卡頓情形 (畫面指標)

手動評估應用程式改善情形

首先,來評估尚未最佳化的應用程式啟動作為對照組。

PACKAGE_NAME=com.example.app

# Force Stop App
adb shell am force-stop $PACKAGE_NAME
# Reset compiled state
adb shell cmd package compile --reset $PACKAGE_NAME

# Measure App startup
# This corresponds to `Time to initial display` metric
# For additional info https://developer.android.com/topic/performance/vitals/launch-time#time-initial
adb shell am start-activity -W -n $PACKAGE_NAME/.ExampleActivity \
 | grep "TotalTime"

接著,側載基準設定檔。

# Unzip the Release APK first
unzip release.apk

# Create a ZIP archive
# Note: The name should match the name of the APK
# Note: Copy baseline.prof{m} and rename it to primary.prof{m}
cp assets/dexopt/baseline.prof primary.prof
cp assets/dexopt/baseline.profm primary.profm

# Create an archive
zip -r release.dm primary.prof primary.profm

# Confirm that release.dm only contains the two profile files:
unzip -l release.dm
# Archive:  release.dm
#   Length      Date    Time    Name
# ---------  ---------- -----   ----
#      3885  1980-12-31 17:01   primary.prof
#      1024  1980-12-31 17:01   primary.profm
# ---------                     -------
#                               2 files

# Install APK + Profile together
adb install-multiple release.apk release.dm

如要確認套件在安裝時是否已完成最佳化,請執行下列指令:

# Check dexopt state
adb shell dumpsys package dexopt | grep -A 1 $PACKAGE_NAME

輸出結果應顯示套件已完成編譯。

[com.example.app]
  path: /data/app/~~YvNxUxuP2e5xA6EGtM5i9A==/com.example.app-zQ0tkJN8tDrEZXTlrDUSBg==/base.apk
  arm64: [status=speed-profile] [reason=install-dm]

現在,可以像之前一樣評估應用程式啟動效能,而且不會影響已編譯的狀態。

# Force Stop App
adb shell am force-stop $PACKAGE_NAME

# Measure App startup
adb shell am start-activity -W -n $PACKAGE_NAME/.ExampleActivity \
 | grep "TotalTime"