使用基准配置文件提升应用性能

1. 前言

在此 Codelab 中,您将学习如何生成基准配置文件以优化应用性能,以及如何验证使用基准配置文件带来的性能优势。

前提条件

此 Codelab 以使用 Macrobenchmark 检查应用性能 Codelab 为基础,后者介绍了如何使用 Macrobenchmark 库衡量应用性能。

所需条件

实践内容

  • 生成基准配置文件以优化性能
  • 使用 Macrobenchmark 库验证性能提升效果

学习内容

  • 生成基准配置文件
  • 了解基准配置文件的性能提升效果

2. 准备工作

首先,从命令行使用以下命令克隆 GitHub 代码库:

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

或者,您也可以下载两个 ZIP 文件:

在 Android Studio 中打开项目

  1. 在“Welcome to 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 库自动生成基准配置文件。如需生成基准配置文件,您可以使用为对应用进行基准测试而创建的模块,只需进行一些额外的更改即可。

为基准配置文件停用混淆处理功能

如果您的应用已启用混淆处理功能,您需要停用此功能以进行基准测试。

为此,您可以将一个额外的 ProGuard 文件添加到您的 :app 模块中,在此模块中停用混淆处理功能,并将 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,因此这行代码会将 benchmark-rules.pro ProGuard 文件添加到发布 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. 查找零食列表的界面元素
  2. 设置手势外边距以免触发系统导航
  3. 滚动列表并等待界面稳定下来
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 模拟器上运行测试,而无需手动启动和拆解模拟器。将定义添加到 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)上使用基准配置文件(这些版本不支持 Cloud 配置文件)。
  • 在没有 Google Play 服务的设备上使用基准配置文件。

最后,点击 1079605eb7639c75.png 图标将项目与 Gradle 文件同步。

40cb2ba3d0b88dd6.png

在下一步中,我们将了解如何验证使用基准配置文件后应用性能的提升幅度。

9. 验证启动性能是否有所提升

现在,我们已经生成了基准配置文件并将其添加到我们的应用中。我们来验证一下它是否对应用性能产生了预期效果。

让我们回到 ExampleStartupBenchmark 类,其中包含用于衡量应用启动的基准。您需要对 startup() 测试略作更改,以便在不同的编译模式下重复使用。这样,您就可以比较使用基准配置文件时的差异了。

CompilationMode

CompilationMode 形参定义了如何将应用预编译为机器代码。它具有以下选项:

  • DEFAULT - 它会使用基准配置文件(如果有)对应用进行部分预编译(如果未应用任何 compilationMode 形参,则使用此选项)
  • None() - 它会重置应用编译状态,且不会预编译应用。在应用执行期间,即时编译 (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 [毫秒]

timeToFullDisplay [毫秒]

364.4

846.5

完整

325.8

739.1

部分

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. 恭喜

恭喜,您已成功完成此 Codelab,并通过使用基准配置文件提升了应用性能!

后续操作

查看我们的性能示例 GitHub 代码库,其中包含 Macrobenchmark 和其他性能示例。此外,请查看 Now In Android 应用示例,这是一个使用基准化分析和基准配置文件来提升性能的真实应用。

参考文档