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

1. 准备工作

此 Codelab 介绍了如何生成基准配置文件以优化应用性能,以及如何验证使用基准配置文件带来的性能优势。

所需条件

实践内容

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

学习内容

  • 基准配置文件以及它们如何提升应用性能。
  • 如何生成基准配置文件。
  • 基准配置文件的性能提升效果。

2. 准备工作

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

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

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

在 Android Studio 中打开项目

  1. 在“Welcome to Android Studio”窗口中,选择 61d0a4432ef6d396.png Open an Existing Project
  2. 选择文件夹 [Download Location]/codelab-android-performance/baseline-profiles。请务必选择 baseline-profiles 目录。
  3. 当 Android Studio 导入项目时,请确保您可以运行 app 模块,以构建您之后使用的示例应用。

示例应用

在此 Codelab 中,您将使用 JetSnack 示例应用。这是一款使用 Jetpack Compose 的虚拟零食订购应用。

为衡量应用的性能,您需要了解界面的结构以及应用的行为方式,以便在基准测试中访问界面元素。请运行此应用,然后通过订购零食来熟悉其中的基本界面。您无需了解与此应用的架构方式相关的详细信息。

23633b02ac7ce1bc.png

3. 什么是基准配置文件

基准配置文件可以避免对包含的代码路径执行解译和即时 (JIT) 编译步骤,从而让代码执行速度从首次启动开始提高约 30%。通过在应用或库中分发基准配置文件,Android 运行时 (ART) 可以通过预先 (AOT) 编译来优化包含的代码路径,从而针对每位新用户以及每个应用更新提升性能。这种配置文件引导的优化 (PGO) 可让应用优化启动、减少互动卡顿,并提高整体的运行时性能,从而让最终用户从首次启动开始便获得更好的使用体验。

使用基准配置文件后,所有用户互动(例如应用启动、切换屏幕或滚动内容)都会从首次运行开始变得更加顺畅。提升应用的速度和响应能力有助于提高日活跃用户数和平均回访率。

基准配置文件会提供常见的用户互动,以便从首次启动开始改进应用运行时,从而引导应用启动之外的优化。引导式 AOT 编译不依赖于用户设备,可以在开发机器(而非移动设备)上为每个版本执行一次。与单纯依赖云配置文件相比,通过使用基准配置文件发布版本,您可以更快速地实现应用优化。

如果不使用基准配置文件,所有应用代码都会在经过解译后在内存中进行 JIT 编译,或在设备处于空闲状态时在后台被编译为 odex 文件。用户安装或更新应用后,首次运行该应用时体验会不太理想,直到新的代码路径得到优化。

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

您可以使用插桩测试类生成基准配置文件,该类要求向您的项目添加新的 Gradle 模块。如要将其添加到项目中,最简便的方法是使用 Android Studio Hedgehog 或更高版本附带的 Android Studio 模块向导。

右键点击 Project 面板中的项目或模块,然后依次选择 New > Module,打开新的模块向导窗口。

232b04efef485e9c.png

在打开的窗口中,从“Templates”窗格中选择 Baseline Profile Generator

b191fe07969e8c26.png

除了模块名称、软件包名称、语言或 build 配置语言等常用参数之外,新模块还有另外两个不常用的输入项:Target applicationUse Gradle Managed Device

Target application 是要生成的基准配置文件所对应的应用模块。如果您的项目中有多个应用模块,请选择您要针对哪个模块运行生成器。

Use Gradle Managed Device 复选框用于将模块设置为在自动管理的 Android 模拟器上运行基准配置文件生成器。如需详细了解 Gradle 管理的设备,请参阅使用 Gradle 管理的设备扩展测试。如果您取消选中此选项,生成器会使用任何已连接的设备。

定义有关新模块的所有详细信息后,点击 Finish 以继续创建模块。

模块向导所做的更改

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

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

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

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

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)上使用基准配置文件,这些版本不支持 Cloud 配置文件。
  • 在没有 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. 查找零食列表的界面元素。
  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 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 之间切换

如需运行此配置,请找到 Generate Baseline Profile 运行配置,然后点击“Run”按钮 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

为此,您可以使用以下插桩测试运行程序实参过滤所有基准配置文件生成器,并跳过所有基准测试:

整个命令如下所示:

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

使用基准配置文件分发应用

生成基准配置文件并将其复制到应用的源代码后,便可以照常构建应用的正式版。您无需执行任何额外操作即可将基准配置文件分发给用户。Android Gradle 插件在构建期间会选择它们并将其包含在 AAB 或 APK 中。接下来,将 build 上传到 Google Play。

当用户安装应用或从以前的版本更新应用时,基准配置文件也会安装,从而使应用在首次运行时实现更好的性能。

下一步介绍了如何验证使用基准配置文件后应用性能的提升效果。

8. (可选)自定义基准配置文件的生成过程

为满足您的特定需求,基准配置文件 Gradle 插件包含用于自定义配置文件生成方式的选项。您可以使用 build 脚本中的 baselineProfile { } 配置块更改行为。

:baselineprofile 模块中的配置块会影响运行生成器的方式,可能会添加 managedDevices,并决定是添加到 useConnectedDevices 还是 Gradle 管理的设备。

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

  • automaticGenerationDuringBuild:如果启用,您可以在构建正式版发布 build 时生成基准配置文件。这对于在交付应用之前基于 CI 进行构建非常有用。
  • saveInSrc:指定生成的基准配置文件是否存储在 src/ 文件夹中。或者,您也可以从 :baselineprofile 构建文件夹访问该文件。
  • baselineProfileOutputDir:定义所生成基准配置文件的存储位置。
  • mergeIntoMain:默认情况下,基准配置文件按 build 变体(产品变种和 build 类型)生成。如果您想将所有配置文件合并到 src/main 中,只需启用此标记即可。
  • filter:您可以过滤要在生成的基准配置文件中包含或排除的类或方法。如果库开发者只想包含来自库的代码,这会非常有用。

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

生成基准配置文件并将其添加到您的应用后,应验证它是否对应用性能产生了预期效果。

新的模块向导会创建一个名为 StartupBenchmarks 的基准测试类。它包含用于衡量应用启动时间的基准测试,并将其与应用在使用基准配置文件情况下启动所用的时间进行比较。

该类如下所示:

@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

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

  • DEFAULT:它会使用基准配置文件(如果有)对应用进行部分预编译。如果未应用任何 compilationMode 形参,则使用此选项。
  • None():它会重置应用编译状态,且不会预编译应用。在应用执行期间,即时编译 (JIT) 仍处于启用状态。
  • Partial():使用基准配置文件和/或预热运行来预编译应用。
  • Full():预编译整个应用代码。这是 Android 6(API 级别 23)及更低版本上的唯一选项。

如果您想开始优化应用性能,可以选择 DEFAULT 编译模式,因为此模式下的性能与从 Google Play 安装应用时相似。如果您想比较基准配置文件提供的性能收益,可以通过比较编译模式 NonePartial 的结果来实现。

修改基准测试以等待内容

基准测试的编写方式与基准配置文件生成器类似,只需编写与应用的互动即可。默认情况下,创建的基准测试只会等待第一帧呈现(类似于 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 [毫秒]

timeToFullDisplay [毫秒]

202.2

818.8

BaselineProfiles

193.7

637.9

改进

4%

28%

timeToFullDisplay 的编译模式之间的差异为 180 毫秒,比只使用基准配置文件大约提高了 28%。CompilationNone 的性能更差,因为设备在应用启动期间必须执行的 JIT 编译最多。CompilationBaselineProfiles 的性能更好,因为使用基准配置文件 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,后者用于衡量生成界面帧所需的时间:

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 会在第 50、第 90、第 95 和第 99 百分位以毫秒 (frameDurationCpuMs) 为单位输出帧时长。在 Android 12(API 级别 31)及更高版本中,它还会返回帧超出限制的时长 (frameOverrunMs)。此值可以为负数,这意味着还有额外的时间可用于生成帧。

从结果中可以看出,CompilationBaselineProfiles 的平均帧时长缩短了 2 毫秒,这对用户而言可能并不明显。但对其他百分位而言,结果就更加明显了。P99 的差值为 43.5 毫秒,相当于在运行的帧速率为 90 FPS 的设备上跳过 3 个以上的帧。例如,对于 Pixel 6,渲染该帧需要的最大时间为 1000 毫秒 / 90 FPS = 约 11 毫秒。

11. 恭喜

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

其他资源

您还可参考以下其他资源:

参考文档