使用基準設定檔改善應用程式效能

Stay organized with collections Save and categorize content based on your preferences.

1. 事前準備

在本程式碼研究室中,您將學習如何產生基準設定檔以便最佳化應用程式效能,並瞭解如何在使用基準設定檔後確認效能改善的程度。

必要條件

這個程式碼研究室是「使用 Macrobenchmark 檢查應用程式效能」程式碼研究室的延伸內容,該程式碼研究室旨在說明如何使用 Macrobenchmark 程式庫評估應用程式效能。

軟硬體需求

執行步驟

  • 產生基準設定檔,以便最佳化效能
  • 運用 Macrobenchmark 程式庫確認效能改善程度

課程內容

  • 產生基準設定檔
  • 瞭解基準設定檔的效能改善程度

2. 開始設定

若要開始,請用以下指令從指令列複製 GitHub 存放區:

$ git clone --branch baselineprofiles-main  https://github.com/googlecodelabs/android-performance.git

或者,您也可以下載兩個 ZIP 檔案:

在 Android Studio 中開啟專案

  1. 在「Welcome to Android Studio」(歡迎使用 Android Studio) 視窗中,選取 c01826594f360d94.png「Open an Existing Project」(開啟現有專案)
  2. 選取資料夾 [Download Location]/android-performance/benchmarking (提示:請確定您選取的是內含 build.gradlebenchmarking 目錄)
  3. Android Studio 匯入專案後,確定您可以藉由執行 app 模組建構要進行基準測試的範例應用程式。

3. 什麼是基準設定檔

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

基準設定檔的運作方式

設定檔規則會以二進位格式編譯到 APK 內 (位於 assets/dexopt/baseline.prof),然後會和 APK 一起 (透過 Google Play) 直接傳輸給使用者。

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

4. 更新基準化模組

應用程式開發人員可以運用 Jetpack Macrobenchmark 程式庫自動產生基準設定檔。您可以用為應用程式進行基準測試的模組產生基準設定檔,只要變更部分內容即可。

停用基準設定檔的模糊處理

如果您的應用程式有啟用模糊處理,則需停用此功能,以便進行基準測試。

若要停用,您可以在 :app 模組內新增 proguard 檔案,然後在該檔案中停用模糊處理,再將這個 proguard 檔案加入 benchmark buildType。

:app 模組內建立新檔案,並命名為 benchmark-rules.pro。這個檔案應該放在 /app/ 資料夾內,位於該模組專用的 build.gradle 檔案旁邊。27bd3b1881011d06.png

在這個檔案中加入 -dontobfuscate 以便停用模糊處理,如以下程式碼片段所示:

# Disables obfuscation for benchmark builds.
-dontobfuscate

然後,修改 :app 模組專用的 build.gradle 中的 benchmark buildType,並加入建立好的檔案。我們使用的是 initWith 版本的 buildType,因此這行程式碼會在版本的 proguard 檔案中加入 benchmark-rules.pro proguard 檔案。

buildTypes {
   release {
      // ...
   }

   benchmark {
      initWith buildTypes.release
      // ...
      proguardFiles('benchmark-rules.pro')
   }
}

現在我們可以撰寫基準設定檔產生器類別了。

5. 撰寫基準設定檔產生器類別

您通常需要為應用程式的一般使用者歷程產生基準試定檔。

在我們的範例中,您可以看到以下三種歷程:

  1. 啟動應用程式 (對大多應用程式都很重要)
  2. 捲動點心清單
  3. 前往點心詳細資料

為了產生基準設定檔,我們會在 :macrobenchmark 模組中加入新的測試類別 BaselineProfileGenerator。這個類別會採用 BaselineProfileRule 測試規則,並含有一個測試方法,藉此產生設定檔。產生設定檔的進入點為 collectBaselineProfile 函式。這個函式只需要兩個參數:

  • packageName,這是應用程式的套件
  • profileBlock (最後的 lambda 參數)
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

   @get:Rule
   val rule = BaselineProfileRule()

   @Test
   fun generate() {
       rule.collectBaselineProfile("com.example.macrobenchmark_codelab") {
           // TODO Add interactions for the typical user journeys
       }
   }
}

您會在 profileBlock lambda 中說明應用程式的一般使用者歷程互動方式。程式庫將執行 profileBlock 數次並收集要進行最佳化的呼叫類別和函式,然後在裝置端產生基準設定檔。

您可以參閱以下程式碼片段,看看我們的基準設定檔如何處理一般歷程:

@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

   @get:Rule
   val rule = BaselineProfileRule()

   @Test
   fun generate() {
       rule.collectBaselineProfile("com.example.macrobenchmark_codelab") {
           startApplicationJourney() // TODO Implement
           scrollSnackListJourney() // TODO Implement
           goToSnackDetailJourney() // TODO Implement
       }
   }
}

接著,我們可以為每個提及的歷程撰寫互動。您可以撰寫為 MacrobenchmarkScope 的擴充功能函式,以便存取其中提供的參數和函式。如果您這樣撰寫,就可以重複利用和基準測試的互動情形,藉此確認效能改善程度。

開始應用程式歷程

應用程式啟動歷程 (startApplicationJourney) 需要處理以下互動:

  1. 按一下主畫面,確認應用程式狀態已經重新啟動
  2. 啟動預設 Activity,等到第一個影格算繪完畢
  3. 等到內容載入並轉譯完畢,而使用者可以進行互動為止
fun MacrobenchmarkScope.startApplicationJourney() {
   pressHome()
   startActivityAndWait()
   val contentList = device.findObject(By.res("snack_list"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(Until.hasObject(By.res("snack_collection")), 5_000)
}

捲動清單歷程

捲動點心清單歷程 (scrollSnackListJourney) 可以按照以下互動操作:

  1. 找到點心清單的 UI 元素
  2. 設定不會觸發系統操作機制的手勢邊界
  3. 捲動清單,並等到 UI 穩定為止
fun MacrobenchmarkScope.scrollSnackListJourney() {
   val snackList = device.findObject(By.res("snack_list"))
   // Set gesture margin to avoid triggering gesture navigation
   snackList.setGestureMargin(device.displayWidth / 5)
   snackList.fling(Direction.DOWN)
   device.waitForIdle()
}

前往詳細資料歷程

最後的歷程 (goToSnackDetailJourney) 會實作以下互動:

  1. 找到您可以使用的點心清單和點心清單項目
  2. 選取清單內的項目
  3. 按一下該項目,並等到詳細資訊畫面載入為止。您可以善用螢幕不會在顯示點心清單這點
fun MacrobenchmarkScope.goToSnackDetailJourney() {
   val snackList = device.findObject(By.res("snack_list"))
   val snacks = snackList.findObjects(By.res("snack_item"))
   // Select random snack from the list
   snacks[Random.nextInt(snacks.size)].click()
   // Wait until the screen is gone = the detail is shown
   device.wait(Until.gone(By.res("snack_list")), 5_000)
}

您已經定義了所有基準設定檔產生器執行所需的互動,不過,您需要先定義執行的裝置。

6. 準備 Gradle 管理的裝置

您需要先準備 userdebug 模擬器,以便產生基準設定檔。您可以使用 Gradle 管理的裝置自動產生基準設定檔。若想瞭解 Gradle 管理的裝置詳情,請參閱我們的說明文件

首先,請用以下程式碼片段在 :macrobenchmark 模組 build.gradle 檔案中定義 Gradle 管理的裝置:

testOptions {
    managedDevices {
        devices {
            pixel2Api31(com.android.build.api.dsl.ManagedVirtualDevice) {
                device = "Pixel 2"
                apiLevel = 31
                systemImageSource = "aosp"
            }
        }
    }
}

產生基準設定檔需要已啟用 Root 權限的 Android 9 (API 28) 以上版本。

我們在此使用 Android 11 (API 級別 31),且 aosp 系統映像檔可以提供 Root 權限。

使用 Gradle 管理的裝置就可以在 Android Emulator 中執行測試,而且不需手動啟動及拆除模擬器。為 build.gradle 新增定義後,即可執行新的 pixel2Api31[BuildVariant]AndroidTest 工作。我們將在下個步驟中用這項工作產生基準設定檔。

7. 測試基準設定檔產生器

Gradle 管理的裝置準備完畢後,即可測試產生器。

從執行設定執行產生器

Gradle 管理的裝置必須用 Gradle 工作執行測試。為了快速開始操作,我們已經建立了執行設定,並指定工作和所有執行所需的參數。

請找到 generateBaselineProfile 執行設定,然後按一下「Run」(執行) 按鈕 229e32fcbe68452f.png 開始執行。

8f6b7c9a5da6585.png

這項測試會建立我們之前定義的模擬器映像檔,並多次執行互動,然後就會拆除模擬器並輸出到 Android Studio。

4b5b2d0091b4518c.png

(非必要) 從指令列執行產生器

您可以利用 Gradle 管理的裝置建立的工作從指令列執行產生器 (:macrobenchmark:pixel2Api31BenchmarkAndroidTest)。

這個指令會執行專案內的所有測試,而由於模組裡也有之後用來確認效能改善程度的基準測試,所以測試會失敗。

因此,您可以用 -P android.testInstrumentationRunnerArguments.class 參數指定您之前撰寫的 com.example.macrobenchmark.BaselineProfileGenerator,就能篩選想執行的類別了。

整體指令如下所示:

./gradlew :macrobenchmark:pixel2Api31BenchmarkAndroidTest -P android.testInstrumentationRunnerArguments.class=com.example.macrobenchmark.BaselineProfileGenerator

8. 套用產生的基準設定檔

產生器成功處理完畢後,您需要再操作幾個步驟,才能讓應用程式使用基準設定檔。

您需要把產生的基準設定檔放到 src/main 資料夾中 (和 AndroidManifest.xml 一起)。您可以到 /macrobenchmark/build/outputs/ 裡面的 managed_device_android_test_additional_output/ 資料夾取得檔案,如以下螢幕截圖所示:

b104f315f06b3578.png

您也可以按一下 Android Studio 輸出內容中的 results 連結並儲存內容,或使用輸出內容中的 adb pull 指令。

接著,您需要把檔案重新命名為 baseline-prof.txt

8973f012921669f6.png

然後請把 profileinstaller 依附元件加入 :app 模組。

dependencies {
  implementation("androidx.profileinstaller:profileinstaller:1.2.0-rc01")
}

藉由加入這項依附元件,您就可以:

  • 在本機為基準設定檔進行基準測試。
  • 在不支援雲端設定檔的 Android 7 (API 級別 24) 及 Android 8 (API 級別 26) 上使用基準設定檔。
  • 在沒有 Google Play 服務的裝置上使用基準設定檔。

最後,按一下 1079605eb7639c75.png 圖示將專案和 Gradle 檔案同步處理。

40cb2ba3d0b88dd6.png

我們將在下個步驟中說明如何確認基準設定檔改善應用程式效能的程度。

9. 確認啟動效能改善程度

如此一來,系統便已產生基準設定檔並將其加入應用程式。現在,就讓我們確認這是否能在應用程式效能方面發揮期望效果。

我們先返回 ExampleStartupBenchmark 類別,這裡內含可以測量應用程式啟動的基準測試。您需要稍加調整 startup() 測試內容,以便在不同的編譯模式下重複使用這個測試。這樣做,即可比較使用基準設定檔前後的不同之處。

CompilationMode

CompilationMode 參數會定義應用程式預先編譯至機器碼的方式。此參數提供以下選項:

  • DEFAULT - 如果可以,系統會使用基準設定檔預先編譯應用程式的部分內容 (如果未套用任何 compilationMode 參數,就會使用此選項)。
  • None() - 重設應用程式編譯狀態,且不會預先編譯應用程式。應用程式執行作業期間依然會啟用 Just in time (JIT) 編譯
  • Partial() – 使用基準設定檔和/或暖身執行預先編譯應用程式。
  • Full() – 預先編譯整個應用程式程式碼。Android 6 (API 23) 以下版本只能使用此選項。

如果想最佳化應用程式效能,可以選擇 DEFAULT 編譯模式,這樣做效能會和從 Google Play 安裝應用程式後的結果相似。如果想比較基準設定檔帶來的效能改善程度,您可以比較 NonePartial 兩種編譯模式的結果。

用其他 CompilationMode 修改啟動測試

首先,移除 startup 方法中的 @Test 註解 (因為 JUnit 測試無法使用參數),然後新增 compilationMode 參數,並用於 measureRepeated 函式:

// Remove @Test annotation and add the compilationMode parameter.
fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
   packageName = "com.google.samples.apps.sunflower",
   metrics = listOf(StartupTimingMetric()),
   iterations = 5,
   compilationMode = compilationMode, // Set the compilation mode
   startupMode = StartupMode.COLD
) {
   pressHome()
   startActivityAndWait()
}

完成後,我們要新增兩個有不同 CompilationMode 的測試函式。第一個測試使用 CompilationMode.None,代表每次在進行基準測試之前,系統會重設應用程式的狀態,因此應用程式沒有任何預先編譯過的程式碼。

@Test
fun startupCompilationNone() = startup(CompilationMode.None())

第二個測試會利用 CompilationMode.Partial,在執行基準測試之前載入基準設定檔,並預先編譯設定檔指定的類別和函式。

@Test
fun startupCompilationPartial() = startup(CompilationMode.Partial())

您也可以再新增第三種方法,並使用 CompilationMode.Full 預先編譯整個應用程式。Android 6 (API 23) 以下版本的系統只能執行完全預先編譯過的應用程式,因此只能使用這個選項。

@Test
fun startupCompilationFull() = startup(CompilationMode.Full())

接下來,請按照之前 (在實體裝置操作) 的方式執行基準測試,直到 Macrobenchmark 用不同的編譯模式測量完啟動時間為止。

基準測試完成後,您就能在 Android Studio 輸出內容中看到時間了,如以下螢幕截圖所示:

cbbc9660374a438.png

您可以在螢幕截圖內看到每個 CompilationMode 都有不同的應用程式啟動時間。中位值如以下表格所示:

timeToInitialDisplay [ms]

timeToFullDisplay [ms]

None (無)

364.4

846.5

Full (完整)

325.8

739.1

Partial (部分)

296.1

708.1

None 編譯的效能理所當然表現最差,因為裝置在啟動應用程式時必須進行大部分的 JIT 編譯。比較令人意外的是 Full 編譯並未得到最佳的效能。因為在這個範例中,所有內容都要經過編譯,應用程式的 odex 檔案非常龐大,因此系統在啟動應用程式時通常要進行更多的 IO。使用基準設定檔的 Partial 例子擁有最佳效能。這是因為部分編譯可以找出平衡點,編譯使用者最有可能使用的程式碼,而不會預先編譯不重要的程式碼,讓系統不必立刻載入這些內容。

10. 確認捲動效能

您可以用和上個步驟類似的方式測量並確認捲動的基準測試。和先前一樣,首先修改 ScrollBenchmarks 類別,在 scroll 測試中加入參數,並新增使用其他編譯模式參數的測試。

開啟 ScrollBenchmarks.kt 檔案,修改 scroll() 函式並新增 compilationMode 參數:

fun scroll(compilationMode: CompilationMode) {
        benchmarkRule.measureRepeated(
            compilationMode = compilationMode, // Set the compilation mode
            // ...

然後,定義多個使用其他參數的測試:

@Test
fun scrollCompilationNone() = scroll(CompilationMode.None())

@Test
fun scrollCompilationPartial() = scroll(CompilationMode.Partial())

用如同之前的方式執行基準測試並取得結果,如以下螢幕截圖所示:249af52e917a4fcf.png

您可以從結果看出 CompilationMode.Partial 的影格時間平均縮短了 0.5 毫秒,雖然使用者可能無法察覺這個差異,不過其他百分位數的結果更加明顯。P90 的差異有 11.7 毫秒,大約等同於影格產生指定時間的 70% (在 Pixel 3 上約為 16 毫秒)。

11. 恭喜

恭喜!您已經成功完成本程式碼研究室,並用基準設定檔改善了應用程式的效能!

後續步驟

請參閱我們的效能範例 GitHub 存放區,其中含有 Macrobenchmark 和其他效能範例。也請參考「Now In Android」範例應用程式 – 這個實際存在的應用程式便是利用基準測試和基準設定檔改善效能。

參考文件