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

1. 开始之前

本 Codelab 演示了如何生成基线配置文件以优化应用程序的性能,以及如何验证使用基线配置文件带来的性能优势。

您需要什么

您将做什么

  • 设置项目以使用基线配置文件生成器。
  • 生成基线配置文件以优化应用启动和滚动性能。
  • 使用 Jetpack Macrobenchmark 库验证性能提升。

您将学到什么

  • 基线配置文件及其如何提升应用性能。
  • 如何生成基线配置文件。
  • 基线配置文件带来的性能提升。

2. 设置

要开始,请使用以下命令从命令行克隆 Github 存储库

$ git clone https://github.com/android/codelab-android-performance.git

或者,您可以下载两个 zip 文件

在 Android Studio 中打开项目

  1. 在“欢迎使用 Android Studio”窗口中,选择 61d0a4432ef6d396.png 打开现有项目
  2. 选择文件夹 [Download Location]/codelab-android-performance/baseline-profiles。确保选择 baseline-profiles 目录。
  3. 当 Android Studio 导入项目时,确保您可以运行 app 模块以构建稍后将使用的示例应用程序。

示例应用

在本 Codelab 中,您将使用 JetSnack 示例 应用程序。它是一个虚拟零食订购应用,使用 Jetpack Compose。

要衡量应用程序的性能,您需要了解 UI 的结构以及应用程序的行为方式,以便您可以从基准测试中访问 UI 元素。运行应用程序,并通过订购零食熟悉基本屏幕。您无需了解应用程序的体系结构细节。

23633b02ac7ce1bc.png

3. 什么是基线配置文件

基线配置文件 通过避免解释和 即时 (JIT) 编译包含代码路径的步骤,将首次启动时的代码执行速度提高约 30%。通过在应用或库中提供基线配置文件,Android 运行时 (ART) 可以通过提前 (AOT) 编译优化包含的代码路径,从而为每个新用户以及每次应用更新提供性能增强。这种配置文件引导优化 (PGO) 允许应用优化启动、减少交互卡顿并提高最终用户从首次启动开始的整体运行时性能。

使用基线配置文件后,所有用户交互(例如应用启动、在屏幕之间导航或滚动内容)从第一次运行开始都更加流畅。提高应用的速度和响应能力可带来更多每日活跃用户,并提高平均回访率。

基线配置文件通过提供常见的用户交互来帮助引导超越应用启动的优化,从而从首次启动开始改善应用运行时。引导式 AOT 编译不依赖于用户设备,可以在开发机器上而非移动设备上针对每个版本执行一次。通过发布包含基线配置文件的版本,应用优化速度比仅依靠 云端配置文件 快得多。

如果不使用基线配置文件,则所有应用代码在被解释后都会在内存中进行 JIT 编译,或者在设备空闲时在后台编译到odex文件中。然后,用户在首次安装或更新后运行应用时可能会遇到次优体验,因为新路径尚未优化。

4. 设置基线配置文件生成器模块

您可以使用一个需要向项目添加新的 Gradle 模块的 Instrumentation 测试类来生成基线配置文件。最简单的方法是使用 Android Studio Hedgehog 或更高版本附带的 Android Studio 模块向导将其添加到您的项目中。

通过右键单击**项目**面板中的项目或模块,然后选择**新建 > 模块**,打开新的模块向导窗口。

232b04efef485e9c.png

在打开的窗口中,从“模板”窗格中选择**基线配置文件生成器**。

b191fe07969e8c26.png

除了模块名称、包名称、语言或构建配置语言等常用参数外,还有两个对于新模块来说不常见的输入:**目标应用程序**和**使用 Gradle 管理的设备**。

**目标应用** 是用于生成基线配置文件的应用模块。如果您的项目中有多个应用模块,请选择要为其运行生成器的模块。

**使用 Gradle 托管设备** 复选框将模块设置为在自动托管的 Android 模拟器上运行基线配置文件生成器。您可以在 使用 Gradle 托管设备扩展您的测试 中详细了解 Gradle 托管设备。如果取消选中此选项,生成器将使用任何已连接的设备。

定义完新模块的所有详细信息后,点击**完成**继续创建模块。

模块向导所做的更改

模块向导会对您的项目进行一些更改。

它会添加一个名为 baselineprofile 或您在向导中选择的名称的 Gradle 模块。

此模块使用 com.android.test 插件,该插件告诉 Gradle 不要将其包含在您的应用程序中,因此它只能包含测试代码或基准测试。它还应用了 androidx.baselineprofile 插件,该插件允许自动生成基线配置文件。

向导还会更改您选择的目标应用程序模块。具体来说,它会应用 androidx.baselineprofile 插件,添加 androidx.profileinstaller 依赖项,并在新创建的模块 build.gradle(.kts) 中添加 baselineProfile 依赖项。

plugins {
  id("androidx.baselineprofile")
}

dependencies {
  // ...
  implementation("androidx.profileinstaller:profileinstaller:1.3.0")
  "baselineProfile"(project(mapOf("path" to ":baselineprofile")))
}

添加 androidx.profileinstaller 依赖项允许您执行以下操作

  • 在本地验证生成的基线配置文件的性能提升。
  • 在不支持云配置文件的 Android 7(API 级别 24)和 Android 8(API 级别 26)上使用基线配置文件。
  • 在没有 Google Play 服务的设备上使用基线配置文件。

baselineProfile(project(":baselineprofile")) 依赖项让 Gradle 知道需要从哪个模块获取生成的基线配置文件。

现在您已设置好项目,请编写一个基线配置文件生成器类。

5. 编写基线配置文件生成器

通常,您会为应用程序的典型用户旅程生成基线配置文件。

模块向导会创建一个基本的 BaselineProfileGenerator 测试类,该类能够为您的应用程序启动生成基线配置文件,如下所示

@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {

    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun generate() {
        rule.collect("com.example.baselineprofiles_codelab") {
            // This block defines the app's critical user journey. This is where you
            // optimize for app startup. You can also navigate and scroll
            // through your most important UI.

            // Start default activity for your app.
            pressHome()
            startActivityAndWait()

            // TODO Write more interactions to optimize advanced journeys of your app.
            // For example:
            // 1. Wait until the content is asynchronously loaded.
            // 2. Scroll the feed content.
            // 3. Navigate to detail screen.

            // Check UiAutomator documentation for more information about how to interact with the app.
            // https://d.android.com/training/testing/other-components/ui-automator
        }
    }
}

此类使用 BaselineProfileRule 测试规则,并包含一个用于生成配置文件的测试方法。生成配置文件的入口点是 collect() 函数。它只需要两个参数

  • packageName:应用程序的包名。
  • profileBlock:最后一个 lambda 参数。

profileBlock lambda 中,您指定涵盖应用程序典型用户旅程的交互。库会运行 profileBlock 多次,收集调用的类和函数,并在设备上生成基线配置文件,其中包含要优化的代码。

默认情况下,创建的生成器类包含启动默认 Activity 的交互,并使用 startActivityAndWait() 方法等待应用程序的第一帧渲染。

使用自定义旅程扩展生成器

您会看到生成的类还包含一些 TODO,用于编写更多交互以优化应用程序的高级旅程。建议您这样做,以便您可以优化应用程序启动之外的性能。

在我们的示例应用程序中,您可以通过执行以下操作来识别这些旅程

  1. 启动应用程序。生成的类已部分涵盖了这一点。
  2. 等待内容异步加载。
  3. 滚动零食列表。
  4. 转到零食详情。

更改生成器以包含以下代码段中概述的涵盖典型旅程的函数

// ...
rule.collect("com.example.baselineprofiles_codelab") {
    // This block defines the app's critical user journey. This is where you
    // optimize for app startup. You can also navigate and scroll
    // through your most important UI.

    // Start default activity for your app.
    pressHome()
    startActivityAndWait()

    // TODO Write more interactions to optimize advanced journeys of your app.
    // For example:
    // 1. Wait until the content is asynchronously loaded.
    waitForAsyncContent()
    // 2. Scroll the feed content.
    scrollSnackListJourney()
    // 3. Navigate to detail screen.
    goToSnackDetailJourney()

    // Check UiAutomator documentation for more information about how to interact with the app.
    // https://d.android.com/training/testing/other-components/ui-automator
}
// ...

现在,为每个提到的旅程编写交互。您可以将其编写为 MacrobenchmarkScope 的扩展函数,以便您可以访问其提供的参数和函数。以这种方式编写可以让您在基准测试中重用交互以验证性能提升。

等待异步内容

许多应用程序在应用程序启动时会进行某种异步加载,也称为 完全显示状态,它告诉系统何时加载和渲染内容,以及用户何时可以与之交互。在生成器 (waitForAsyncContent) 中使用这些交互等待该状态

  1. 查找 Feed 零食列表。
  2. 等待列表中的一些项目在屏幕上可见。
fun MacrobenchmarkScope.waitForAsyncContent() {
   device.wait(Until.hasObject(By.res("snack_list")), 5_000)
   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 snack from the list based on running iteration.
    val index = (iteration ?: 0) % snacks.size
    snacks[index].click()
    // Wait until the screen is gone = the detail is shown.
    device.wait(Until.gone(By.res("snack_list")), 5_000)
}

定义完基线配置文件生成器准备好运行所需的所有交互后,您需要定义其运行的设备。

6. 准备运行生成器的设备

要生成基线配置文件,我们建议使用模拟器(例如 Gradle 托管设备)或运行 Android 13(API 33)或更高版本的设备。

为了使流程可重现并自动生成基线配置文件,您可以使用 Gradle 托管设备。Gradle 托管设备允许您在 Android 模拟器上运行测试,而无需手动启动和关闭它。您可以在 使用 Gradle 托管设备扩展您的测试 中详细了解 Gradle 托管设备。

要定义 Gradle 托管设备,请将其定义添加到 :baselineprofile 模块 build.gradle.kts 文件中,如下面的代码段所示

android {
  // ...

  testOptions.managedDevices.devices {
    create<ManagedVirtualDevice>("pixel6Api31") {
        device = "Pixel 6"
        apiLevel = 31
        systemImageSource = "aosp"
    }
  } 
}

在这种情况下,我们使用 Android 11(API 级别 31),并且 aosp 系统映像能够进行 root 访问。

接下来,配置基线配置文件 Gradle 插件以使用已定义的 Gradle 托管设备。为此,请将设备名称添加到 managedDevices 属性中,并禁用 useConnectedDevices,如下面的代码段所示

android {
  // ...
}

baselineProfile {
   managedDevices += "pixel6Api31"
   useConnectedDevices = false
}

dependencies {
  // ...
}

接下来,生成基线配置文件。

7. 生成基线配置文件

设备准备就绪后,您可以创建基线配置文件。基线配置文件 Gradle 插件会创建 Gradle 任务来自动执行运行生成器测试类并将生成的基线配置文件应用到应用程序的整个过程。

新的模块向导创建了运行配置,以便能够快速运行 Gradle 任务以及所有必要的参数以运行,而无需在终端和 Android Studio 之间切换

要运行它,请找到 生成基线配置文件运行配置并点击运行按钮 599be5a3531f863b.png

6911ecf1307a213f.png

该任务启动之前定义的模拟器映像。运行 BaselineProfileGenerator 测试类中的交互多次,然后拆除模拟器并将输出提供给 Android Studio。

生成器成功完成运行后,Gradle 插件会自动将生成的 baseline-prof.txt 放到目标应用程序(:app 模块)的 src/release/generated/baselineProfile/ 文件夹中。

fa0f52de5d2ce5e8.png

(可选)从命令行运行生成器

或者,您可以从命令行运行生成器。您可以利用 Gradle 托管设备创建的任务——:app:generateBaselineProfile。此命令运行由 baselineProfile(project(:baselineProfile)) 依赖项定义的项目中的所有测试。因为该模块还包含用于稍后验证性能提升的基准测试,所以这些测试会失败,并显示一条警告,提示不要在模拟器上运行基准测试。

android
   .testInstrumentationRunnerArguments
   .androidx.benchmark.enabledRules=BaselineProfile

为此,您可以使用以下 instrumentation runner 参数过滤所有基线配置文件生成器,并且所有基准测试都将被跳过

整个命令如下所示

./gradlew :app:generateBaselineProfile -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile

分发包含基线配置文件的应用程序

生成基线配置文件并将其复制到应用程序的源代码后,像往常一样构建应用程序的生产版本。您无需执行任何额外操作即可将基线配置文件分发给用户。它们在构建期间由 Android Gradle 插件选取并包含在您的 AAB 或 APK 中。接下来,将构建上传到 Google Play。

当用户安装应用程序或从先前版本更新应用程序时,基线配置文件也会随之安装,从而使应用程序从首次运行开始就获得更好的性能。

下一步将展示如何验证应用程序性能使用基线配置文件后提升了多少。

8. (可选)自定义生成基线配置文件

Baseline Profiles Gradle 插件包含自定义生成配置文件以满足您特定需求的选项。您可以在构建脚本中的 baselineProfile { } 配置块中更改行为。

位于 :baselineprofile 模块内的配置块会影响如何运行生成器,并可以选择添加 managedDevices 以及决定是使用 useConnectedDevices 还是 Gradle 管理的设备。

位于 :app 目标模块内的配置块决定了配置文件的保存位置或生成方式。您可以更改以下参数

  • automaticGenerationDuringBuild:如果启用,您可以在构建生产发布版本时生成 Baseline Profile。这在 CI 上构建应用程序之前发布时非常有用。
  • saveInSrc:指定是否将生成的 Baseline Profile 存储在 src/ 文件夹中。或者,您可以从 :baselineprofile 构建文件夹访问该文件。
  • baselineProfileOutputDir:定义存储生成的 Baseline Profile 的位置。
  • mergeIntoMain:默认情况下,Baseline Profile 会根据构建变体(产品风格和构建类型)生成。如果您希望将所有配置文件合并到 src/main 中,可以通过启用此标志来实现。
  • filter:您可以过滤要包含或排除在生成的 Baseline Profile 中的类或方法。这对于只想包含库代码的库开发者很有用。

9. 验证启动性能改进

生成 Baseline Profile 并将其添加到您的应用后,请验证它是否对您的应用性能产生了预期的效果。

新的模块向导会创建一个名为 StartupBenchmarks 的基准测试类。它包含一个基准测试,用于测量 应用启动时间 并将其与应用使用 Baseline Profile 时的启动时间进行比较。

该类的外观如下所示

@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupBenchmarks {

    @get:Rule
    val rule = MacrobenchmarkRule()

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

    @Test
    fun startupCompilationBaselineProfiles() =
        benchmark(CompilationMode.Partial(BaselineProfileMode.Require))

    private fun benchmark(compilationMode: CompilationMode) {
        rule.measureRepeated(
            packageName = "com.example.baselineprofiles_codelab",
            metrics = listOf(StartupTimingMetric()),
            compilationMode = compilationMode,
            startupMode = StartupMode.COLD,
            iterations = 10,
            setupBlock = {
                pressHome()
            },
            measureBlock = {
                startActivityAndWait()

                // TODO Add interactions to wait for when your app is fully drawn.
                // The app is fully drawn when Activity.reportFullyDrawn is called.
                // For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter
                // from the AndroidX Activity library.

                // Check the UiAutomator documentation for more information on how to
                // interact with the app.
                // https://d.android.com/training/testing/other-components/ui-automator
            }
        )
    }
}

它使用 MacrobenchmarkRule,该规则能够为您的应用运行基准测试并收集性能指标。编写基准测试的入口点是规则中的 measureRepeated 函数。

它需要几个参数

  • packageName: 要测量的应用程序。
  • metrics:在基准测试期间要测量的 信息类型
  • iterations:基准测试重复的次数。
  • startupMode:您希望您的应用程序在基准测试开始时 如何启动
  • setupBlock:在测量之前必须与您的应用进行的交互。
  • measureBlock:在基准测试期间您想要测量的与您的应用的交互。

测试类还包含两个测试:startupCompilationeNone()startupCompilationBaselineProfiles(),它们使用不同的 compilationMode 调用 benchmark() 函数。

编译模式

CompilationMode 参数定义了应用程序如何预编译成机器代码。它具有以下选项

  • DEFAULT:如果可用,则使用 Baseline Profile 部分预编译应用程序。如果未应用 compilationMode 参数,则使用此选项。
  • None():重置应用程序编译状态,不预编译应用程序。在应用程序执行期间仍然启用 即时编译 (JIT)。
  • Partial():使用 Baseline Profile 或预热运行(或两者)预编译应用程序。
  • Full():预编译整个应用程序代码。这是 Android 6(API 23)及更低版本上的唯一选项。

如果您想开始优化应用程序性能,可以选择 DEFAULT 编译模式,因为性能与从 Google Play 安装应用程序时的性能类似。如果您想比较 Baseline Profile 提供的性能优势,可以通过比较 NonePartial 编译模式的结果来实现。

修改基准测试以等待内容

基准测试的编写方式与 Baseline Profile 生成器类似,通过编写与您的应用的交互来实现。默认情况下,创建的基准测试仅等待渲染第一帧——类似于 BaselineProfileGenerator 的方式——因此我们建议将其改进为等待异步内容。

您可以通过重用为生成器编写的扩展函数来实现。由于此基准测试通过使用 StartupTimingMetric() 捕获启动时间,因此我们建议您只在此处包含等待异步内容的操作,然后为生成器中定义的其他用户旅程编写单独的基准测试。

// ...
measureBlock = {
   startActivityAndWait()

   // The app is fully drawn when Activity.reportFullyDrawn is called.
   // For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter
   // from the AndroidX Activity library.
   waitForAsyncContent() // <------- Added to wait for async content.

   // Check the UiAutomator documentation for more information on how to
   // interact with the app.
   // https://d.android.com/training/testing/other-components/ui-automator
}

运行基准测试

您可以像运行检测测试一样运行基准测试。您可以运行测试函数,或使用旁边的槽图标运行整个类。

587b04d1a76d1e9d.png

确保已选择物理设备,因为在 Android 模拟器上运行基准测试会在运行时失败,并显示一条警告,指出基准测试可能会给出不正确的结果。虽然您可以在技术上在模拟器上运行它,但您正在测量主机机的性能。如果它处于高负载状态,您的基准测试会执行得更慢,反之亦然。

94e0da86b6f399d5.png

运行基准测试后,您的应用将重新构建,然后运行您的基准测试。根据您定义的 iterations,基准测试会启动、停止甚至重新安装您的应用多次。

基准测试完成后,您可以在 Android Studio 输出中看到时间,如下面的屏幕截图所示

282f90d5f6ff5196.png

从屏幕截图中,您可以看到每个 CompilationMode 的应用启动时间都不同。中位数显示在下表中

timeToInitialDisplay [ms]

timeToFullDisplay [ms]

None

202.2

818.8

BaselineProfiles

193.7

637.9

改进

4%

28%

对于 timeToFullDisplay,编译模式之间的差异为 180ms,仅通过使用 Baseline Profile 即可提高约 28% 的性能。 CompilationNone 的性能较差,因为设备必须在应用启动期间执行最多的 JIT 编译。 CompilationBaselineProfiles 的性能更好,因为使用 Baseline Profile 进行部分编译会 AOT 编译用户最可能使用的代码,并将非关键代码不进行预编译,因此它不必立即加载。

10. (可选)验证滚动性能改进

与上一步类似,您可以测量和验证滚动性能。首先,使用基准测试规则和两个使用不同编译模式的测试方法创建 ScrollBenchmarks 测试类

@LargeTest
@RunWith(AndroidJUnit4::class)
class ScrollBenchmarks {

   @get:Rule
   val rule = MacrobenchmarkRule()

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

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

   private fun scroll(compilationMode: CompilationMode) {
       // TODO implement
   }
}

scroll 方法中,使用 measureRepeated 函数及其所需的参数。对于 metrics 参数,使用 FrameTimingMetric,它测量生成 UI 帧所需的时间

private fun scroll(compilationMode: CompilationMode) {
   rule.measureRepeated(
       packageName = "com.example.baselineprofiles_codelab",
       metrics = listOf(FrameTimingMetric()),
       compilationMode = compilationMode,
       startupMode = StartupMode.WARM,
       iterations = 10,
       setupBlock = {
           // TODO implement
       },
       measureBlock = {
           // TODO implement
       }
   )
}

这次,您需要在 setupBlockmeasureBlock 之间进一步划分交互,以便仅在第一次布局和滚动内容期间测量帧持续时间。因此,将启动默认屏幕的函数放在 setupBlock 中,并将已创建的扩展函数 waitForAsyncContent()scrollSnackListJourney() 放在 measureBlock

private fun scroll(compilationMode: CompilationMode) {
   rule.measureRepeated(
       packageName = "com.example.baselineprofiles_codelab",
       metrics = listOf(FrameTimingMetric()),
       compilationMode = compilationMode,
       startupMode = StartupMode.WARM,
       iterations = 10,
       setupBlock = {
           pressHome()
           startActivityAndWait()
       },
       measureBlock = {
           waitForAsyncContent()
           scrollSnackListJourney()
       }
   )
}

基准测试准备就绪后,您可以像以前一样运行它以获取结果,如下面的屏幕截图所示

84aa99247226fc3a.png

FrameTimingMetric 以毫秒为单位输出帧的持续时间(frameDurationCpuMs),包括第 50、90、95 和 99 百分位数。在 Android 12(API 级别 31)及更高版本上,它还会返回帧超出限制的时间(frameOverrunMs)。该值可以为负数,表示有额外的时间可以生成帧。

从结果中,您可以看到 CompilationBaselineProfiles 平均帧持续时间缩短了 2ms,用户可能不会注意到。但是,对于其他百分位数,结果更加明显。对于 P99,差异为 43.5ms,这在以 90 FPS 运行的设备上超过了 3 个跳帧。例如,对于 Pixel 6,渲染一帧的最大时间为 1000ms / 90 FPS = ~11ms。

11. 恭喜

恭喜您成功完成了本代码实验室,并使用 Baseline Profile 提高了应用的性能!

其他资源

请参阅以下其他资源

参考文档