1. 准备工作
在此 Codelab 中,您将学习如何生成基准配置文件以优化应用性能,以及如何验证使用基准配置文件带来的性能优势。
前提条件
此 Codelab 以使用 Macrobenchmark 检查应用性能 Codelab 为基础,后者介绍了如何使用 Macrobenchmark 库衡量应用性能。
所需条件
- Android Studio Dolphin (2021.3.1) 或更高版本
- 了解 Kotlin
- 对 Jetpack Macrobenchmark 有基本的了解
- 一台搭载 Android 6(API 级别 23)或更高版本的 Android 实体设备
- 不带 Google Play 且搭载 Android 9(API 级别 28)或更高版本的 Android 模拟器
实践内容
- 生成基准配置文件以优化性能
- 使用 Macrobenchmark 库验证性能提升效果
学习内容
- 生成基准配置文件
- 了解基准配置文件的性能提升效果
2. 准备工作
首先,从命令行使用以下命令克隆 GitHub 代码库:
$ git clone --branch baselineprofiles-main https://github.com/googlecodelabs/android-performance.git
或者,您也可以下载两个 ZIP 文件:
在 Android Studio 中打开项目
- 在“Welcome to Android Studio”窗口中,选择
Open an Existing Project
- 选择文件夹
[Download Location]/android-performance/benchmarking
(提示:务必选择包含build.gradle
的benchmarking
目录) - Android Studio 导入项目后,请确保您可以运行用于构建我们要进行基准测试的示例应用的
app
模块。
3. 什么是基准配置文件
基准配置文件可以避免对包含的代码路径执行解译和即时 (JIT) 编译步骤,从而让代码执行速度从首次启动开始提高约 30%。通过在应用或库中分发基准配置文件,Android 运行时 (ART) 可以通过预先 (AOT) 编译来优化包含的代码路径,从而针对每位新用户以及每个应用更新提升性能。这种配置文件引导的优化 (PGO) 可让应用优化启动、减少互动卡顿,并提高整体的运行时性能,从而让用户从首次启动开始便获得更好的使用体验。
基准配置文件的优势
使用基准配置文件后,所有用户互动(例如应用启动、切换屏幕或滚动内容)都会从首次运行开始变得更加顺畅。提升应用的速度和响应能力有助于提高日活跃用户数和平均回访率。
基准配置文件会提供常见的用户互动,以便从首次启动开始改进应用运行时,从而引导应用启动之外的优化。引导式 AOT 编译不依赖于用户设备,可以在开发机器(而非移动设备)上为每个版本执行一次。与单纯依赖云配置文件相比,通过使用基准配置文件发布版本,您可以更快速地实现应用优化。
如果不使用基准配置文件,所有应用代码都会在经过解译后在内存中进行 JIT 编译,或在设备处于空闲状态时在后台被编译为 odex 文件。刚刚安装或更新了某个应用的用户无法在首次运行时获得理想的体验,并且这种情况会持续到新路径完成优化。许多应用的实测性能提升幅度都达到了 30% 左右。
4. 设置基准化分析模块
作为应用开发者,您可以使用 Jetpack Macrobenchmark 库自动生成基准配置文件。如需生成基准配置文件,您可以使用为对应用进行基准测试而创建的模块,只需进行一些额外的更改即可。
为基准配置文件停用混淆处理功能
如果您的应用已启用混淆处理功能,您需要停用此功能以进行基准测试。
为此,您可以将一个额外的 ProGuard 文件添加到您的 :app
模块中,在此模块中停用混淆处理功能,并将 ProGuard 文件添加到 benchmark
buildType。
在 :app
模块中创建一个名为 benchmark-rules.pro
的新文件。此文件应放置在 /app/
文件中的模块专用 build.gradle
文件旁边。
在此文件中,通过添加 -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. 编写基准配置文件生成器
通常,您要为应用的典型用户体验历程生成基准配置文件。
在本例中,您可以标识以下三种历程:
- 启动应用(对于大多数应用来说,这至关重要)
- 滚动零食列表
- 进入零食详情页面
为了生成基准配置文件,我们将在 :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
),您需要涵盖以下互动:
- 按主屏幕按钮以确保应用状态已重启
- 启动默认 activity 并等待第一帧呈现
- 等待内容加载并呈现,且用户能够与其互动
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
),您可以遵循以下互动:
- 查找零食列表的界面元素
- 设置手势外边距以免触发系统导航
- 滚动列表并等待界面稳定下来
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
) 实现了以下互动:
- 查找零食列表,以及所有可使用的零食项
- 从列表中选择一个项
- 点击此项,然后等待详情屏幕加载。您可以利用这一事实:零食列表不会再显示在屏幕上
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”按钮 。
此测试将创建之前定义的模拟器映像,多次运行互动,之后拆解模拟器并向 Android Studio 提供输出。
(可选)通过命令行运行生成器
如需通过命令行运行生成器,您可以利用 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/
文件夹进行复制,如以下屏幕截图所示。
或者,您也可以点击 Android Studio 输出结果中的 results
链接并保存内容,或使用输出结果中输出的 adb pull
命令。
接下来,您需要将此文件重命名为 baseline-prof.txt
。
然后,将 profileinstaller
依赖项添加到您的 :app
模块。
dependencies {
implementation("androidx.profileinstaller:profileinstaller:1.2.0")
}
添加此依赖项后,您可以:
- 在本地对基准配置文件进行基准测试。
- 在 Android 7(API 级别 24)和 Android 8(API 级别 26)上使用基准配置文件(这些版本不支持 Cloud 配置文件)。
- 在没有 Google Play 服务的设备上使用基准配置文件。
最后,点击 图标将项目与 Gradle 文件同步。
在下一步中,我们将了解如何验证使用基准配置文件后应用性能的提升幅度。
9. 验证启动性能是否有所提升
现在,我们已经生成了基准配置文件并将其添加到我们的应用中。我们来验证一下它是否对应用性能产生了预期效果。
让我们回到 ExampleStartupBenchmark
类,其中包含用于衡量应用启动的基准。您需要对 startup()
测试略作更改,以便在不同的编译模式下重复使用。这样,您就可以比较使用基准配置文件时的差异了。
CompilationMode
CompilationMode
形参定义了如何将应用预编译为机器代码。它具有以下选项:
DEFAULT
- 它会使用基准配置文件(如果有)对应用进行部分预编译(如果未应用任何compilationMode
参数,则使用此选项)None()
- 它会重置应用编译状态,且不会预编译应用。在应用执行期间,即时编译 (JIT) 仍处于启用状态。Partial()
- 它会使用基准配置文件和/或预热运行来预编译应用。Full()
- 它会预编译整个应用代码。这是 Android 6(API 级别 23)及更低版本上的唯一选项。
如果您想开始优化应用性能,可以选择 DEFAULT
编译模式,因为此模式下的性能与从 Google Play 安装应用时相似。如果您想比较基准配置文件提供的性能收益,可以通过比较编译模式 None
和 Partial
的结果来实现。
使用不同的 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 输出结果中查看相关时间,如以下屏幕截图所示:
从屏幕截图中可以看出,每个 CompilationMode
的应用启动时间都不一样。下表显示的是中间值:
timeToInitialDisplay [毫秒] | timeToFullDisplay [毫秒] | |
无 | 396.8 | 818.1 |
完整 | 373.9 | 755.0 |
部分 | 352.9 | 720.9 |
直观地说,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())
然后像之前一样运行基准,如下面的屏幕截图所示:
从结果中可以看出,CompilationMode.Partial
的平均帧时间缩短了 0.4 毫秒,这对用户而言可能并不明显,但对其他百分位而言,结果就更加明显了。P99 的差值为 36.9 毫秒,相当于跳过 3 个以上的帧(Pixel 7 运行的帧速率为 90 FPS,约为 11 毫秒)。
11. 恭喜
恭喜,您已成功完成此 Codelab,并通过使用基准配置文件提升了应用性能!
后续操作
查看我们的性能示例 GitHub 代码库,其中包含 Macrobenchmark 和其他性能示例。此外,请查看 Now In Android 应用示例,这是一个使用基准化分析和基准配置文件来提升性能的真实应用。