使用基准配置文件改进应用性能

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 的虚拟零食订购应用。

要测量应用的性能,你需要了解 UI 的结构和应用的表现,以便从基准测试中访问 UI 元素。运行应用并熟悉通过订购零食的基本屏幕。你无需了解应用架构的细节。

23633b02ac7ce1bc.png

3. 什么是基准配置文件

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

通过基准配置文件,所有用户交互(例如应用启动、屏幕之间的导航或内容滚动)从首次运行时起都更加流畅。提高应用的速度和响应能力会带来更多的日活跃用户和更高的平均回访率。

基准配置文件通过提供常见的用户交互来帮助指导除应用启动之外的优化,从而从首次启动起改进应用运行时性能。引导 AOT 编译不依赖于用户设备,可以在开发机器上进行,而不是在移动设备上,并且每个版本只需执行一次。通过发布包含基准配置文件的版本,应用优化比仅依赖云配置文件更快地可用。

在不使用基准配置文件时,所有应用代码在解释后会在内存中进行 JIT 编译,或者在设备空闲时在后台编译成 odex 文件。用户在首次安装或更新应用后,在新的路径被优化之前,可能会遇到次优体验。

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

你可以使用需要向项目添加新 Gradle 模块的 instrumentation test class 来生成基准配置文件。将其添加到项目的最简单方法是使用 Android Studio Hedgehog 或更高版本附带的 Android Studio 模块向导。

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

232b04efef485e9c.png

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

b191fe07969e8c26.png

除了模块名称、包名、语言或构建配置语言等常规参数外,对于新模块还有两个不寻常的输入:Target applicationUse Gradle Managed Device

Target application 是用于为其生成基准配置文件的应用模块。如果你的项目中有多个应用模块,选择你想要为其运行生成器的一个。

Use Gradle Managed Device 复选框将模块设置为在自动管理的 Android 模拟器上运行基准配置文件生成器。你可以在使用 Gradle Managed Devices 扩展测试中阅读更多关于 Gradle Managed Devices 的信息。如果取消选中此选项,生成器将使用任何连接的设备。

定义完新模块的所有详细信息后,点击 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) 上使用基准配置文件,这些版本不支持云配置文件。
  • 在没有 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 Managed Device)或运行 Android 13 (API 33) 或更高版本的设备。

为了使流程可重现并自动化生成基准配置文件,你可以使用 Gradle Managed Devices。Gradle Managed Devices 允许你在 Android 模拟器上运行测试,而无需手动启动和关闭它。你可以在使用 Gradle Managed Devices 扩展测试中了解更多关于 Gradle Managed Devices 的信息。

要定义 Gradle Managed Device,请将它的定义添加到 :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 Managed Device。为此,将设备的名称添加到 managedDevices 属性中,并禁用 useConnectedDevices,如下面的片段所示

android {
  // ...
}

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

dependencies {
  // ...
}

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

7. 生成基准配置文件

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

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

要运行它,找到 Generate Baseline Profile 运行配置并点击运行按钮 599be5a3531f863b.png

6911ecf1307a213f.png

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

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

fa0f52de5d2ce5e8.png

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

此外,你可以从命令行运行生成器。你可以利用 Gradle Managed Device 创建的任务——: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.(可选)自定义基准配置文件生成

基准配置文件 Gradle 插件包含用于自定义如何生成配置文件以满足你特定需求的选项。你可以在构建脚本中使用 baselineProfile { } 配置块来更改行为。

:baselineprofile 模块中的配置块影响如何运行生成器,并可以添加 managedDevices 并决定是 useConnectedDevices 还是使用 Gradle Managed Devices。

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

  • automaticGenerationDuringBuild:如果启用,可以在构建生产发布版本时生成基准配置文件。这在 CI 上构建应用程序之前非常有用。
  • saveInSrc:指定生成的基准配置文件是否存储在 src/ 文件夹中。或者,你可以从 :baselineprofile 构建文件夹中访问文件。
  • baselineProfileOutputDir:定义存储生成的基准配置文件的位置。
  • mergeIntoMain:默认情况下,基准配置文件按构建变体(产品特性和构建类型)生成。如果你想将所有配置文件合并到 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
}

运行基准测试

你可以像运行插装测试一样运行基准测试。你可以运行测试函数,或运行整个类(使用旁边的 gutter 图标)。

587b04d1a76d1e9d.png

请确保你选择的是实体设备,因为在 Android 模拟器上运行基准测试会在运行时失败,并发出警告,表示基准测试可能会给出不正确的结果。虽然技术上可以在模拟器上运行,但你测量的是宿主机器的性能。如果负载过重,你的基准测试会表现得更慢,反之亦然。

94e0da86b6f399d5.png

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

基准测试完成后,你可以在 Android Studio 输出中看到计时信息,如下所示截图

282f90d5f6ff5196.png

从截图中可以看出,不同 CompilationMode 的应用启动时间是不同的。中位数如下表所示

timeToInitialDisplay [毫秒]

timeToFullDisplay [毫秒]

202.2

818.8

基准配置文件

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,它测量生成 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 的平均帧持续时间缩短了 2 毫秒,这对于用户来说可能不太明显。然而,对于其他百分位数,结果更明显。对于 P99,差异为 43.5 毫秒,这在 90 FPS 设备上超过了 3 个跳过的帧。例如,对于 Pixel 6,1000 毫秒 / 90 FPS = ~11 毫秒的最大帧渲染时间。

11. 恭喜

恭喜,你已成功完成本 Codelab,并使用基准配置文件提升了应用的性能!

其他资源

请参阅以下其他资源

参考文档