1. 准备工作
在此 Codelab 中,您将学习使用 Macrobenchmark 库。您将测量应用启动时间(用户参与度的关键指标)和帧时间(可提示应用可能发生卡顿的位置)。
您需要准备什么
- Android Studio
- Kotlin 知识
- 对 Android 上的测试有基本了解
- 搭载 Android 6 (API 级别 23) 或更高版本的真机 Android 设备
您将做什么
- 向现有应用添加基准测试模块
- 测量应用启动和帧时间
您将学到什么
- 可靠地测量应用性能
2. 设置
首先,使用以下命令从命令行克隆 Github 仓库
$ git clone https://github.com/android/codelab-android-performance.git
另外,您也可以下载两个 zip 文件
在 Android Studio 中打开项目
- 在 Android Studio 欢迎窗口中选择
打开现有项目
- 选择文件夹
[Download Location]/android-performance/benchmarking
(提示:确保选择包含build.gradle
的benchmarking
目录) - Android Studio 导入项目后,请确保您可以运行
app
模块来构建我们将要进行基准测试的示例应用。
3. Jetpack Macrobenchmark 简介
Jetpack Macrobenchmark 库用于衡量大型端用户交互的性能,例如应用启动、与 UI 交互以及动画。该库提供对您正在测试的性能环境的直接控制。它允许您控制应用的编译、启动和停止,从而直接衡量应用启动时间、帧时间以及跟踪的代码段。
借助 Jetpack Macrobenchmark,您可以
- 通过确定的启动模式和滚动速度多次测量应用
- 通过对多次测试运行的结果取平均值来平滑性能差异
- 控制应用的编译状态 - 影响性能稳定性的主要因素
- 通过本地复现 Google Play 商店执行的安装时优化来检查实际性能
使用此库的检测不会直接调用您的应用代码,而是像用户一样导航您的应用 - 轻触、点击、滑动等。测量是在设备上进行这些交互期间发生的。如果您想直接测量应用代码的某些部分,请参阅 Jetpack Microbenchmark。
编写基准测试就像编写检测测试一样,只是您无需验证应用所处的状态。基准测试使用 JUnit 语法(@RunWith
、@Rule
、@Test
等),但测试在单独的进程中运行,以允许重新启动或预编译您的应用。这使我们能够在不干扰应用内部状态的情况下运行它,就像用户一样。我们通过使用 UiAutomator
与目标应用进行交互来做到这一点。
示例应用
在此 Codelab 中,您将使用 JetSnack 示例应用。这是一个使用 Jetpack Compose 的虚拟零食订购应用。要衡量应用的性能,您无需了解应用的架构细节。您需要了解的是应用的行为方式和 UI 结构,以便从基准测试中访问 UI 元素。运行应用并通过订购您选择的零食来熟悉基本屏幕。
4. 添加 Macrobenchmark 库
Macrobenchmark 要求向您的项目添加一个新的 Gradle 模块。将其添加到项目中最简单的方法是使用 Android Studio 模块向导。
打开新建模块对话框(例如,在项目面板中右键点击您的项目或模块,然后选择新建 > 模块)。
从模板窗格中选择 Benchmark,确保 Macrobenchmark 被选为基准测试模块类型,并检查详细信息是否符合您的预期
- 目标应用 – 将进行基准测试的应用
- 模块名称 – 基准测试 Gradle 模块的名称
- 包名称 – 基准测试的包名称
- 最低 SDK – 需要 Android 6 (API 级别 23) 或更高版本
点击完成。
模块向导所做的更改
模块向导会对您的项目进行一些更改。
它添加了一个名为 macrobenchmark
的 Gradle 模块(或者您在向导中选择的名称)。此模块使用 com.android.test
插件,该插件会告诉 Gradle 不要将其包含在您的应用中,因此它只能包含测试代码(或基准测试)。
向导还会更改您选择的目标应用模块。具体来说,它会向 :app
模块的 build.gradle.kts
添加新的 benchmark
构建类型,如以下代码段所示
create("benchmark") {
initWith(buildTypes.getByName("release"))
matchingFallbacks += listOf("release")
isDebuggable = false
}
此构建类型应尽可能地模拟您的 release
构建类型。与 release
构建类型的区别在于 signingConfig
设置为 debug
,这只是为了让您无需生产密钥库即可在本地构建应用。
但是,由于 debuggable
标志被禁用,向导会将 <profileable>
标签添加到您的 AndroidManifest.xml
中,以允许基准测试在发布性能下对您的应用进行性能分析。
<application>
<profileable
android:shell="true"
tools:targetApi="29" />
</application>
要获取有关 <profileable>
功能的更多信息,请查看我们的文档。
向导做的最后一件事是创建一个用于基准测试启动时间的脚手架(我们将在下一步中使用)。
现在您已准备好开始编写基准测试。
5. 测量应用启动时间
应用启动时间,即用户开始使用您的应用所需的时间,是影响用户参与度的关键指标。模块向导会创建一个 ExampleStartupBenchmark
测试类,该类能够测量您的应用启动时间,其代码如下
@RunWith(AndroidJUnit4::class)
class ExampleStartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.macrobenchmark_codelab",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD,
){
pressHome()
startActivityAndWait()
}
}
所有参数是什么意思?
编写基准测试时,入口点是 MacrobenchmarkRule
的 measureRepeated
函数。此函数会处理基准测试的所有事务,但您需要指定这些参数
packageName
– 基准测试与被测应用在单独的进程中运行,因此您需要指定要测量的应用。metrics
– 在基准测试期间您想要测量的信息类型。在我们的例子中,我们对应用启动时间感兴趣。有关其他类型的指标,请查看文档。iterations
– 基准测试将重复多少次。迭代次数越多意味着结果越稳定,但会以更长的执行时间为代价。理想的次数取决于此特定指标对于您的应用的噪声程度。startupMode
– 允许您定义在基准测试开始时您的应用应如何启动。可用模式有COLD
、WARM
和HOT
。我们使用COLD
,因为它代表了应用必须执行的最大工作量。measureBlock
(最后一个 lambda 参数)– 在此函数中,您定义在基准测试期间要测量的操作(启动 Activity、点击 UI 元素、滚动、滑动等),macrobenchmark 将在此块期间收集定义的metrics
。
如何编写基准测试操作
Macrobenchmark 将重新安装并重新启动您的应用。确保您编写的交互独立于应用的状态。Macrobenchmark 提供了几个有用的函数和参数来与您的应用进行交互。
最重要的函数是 startActivityAndWait()
。此函数将启动您的默认 Activity 并等待直到它渲染第一个帧,然后才继续执行基准测试中的指令。如果您想启动不同的 Activity 或调整启动 Intent,可以使用可选的 intent
或 block
参数来执行此操作。
另一个有用的函数是 pressHome()
。这允许您在每次迭代不杀死应用的情况下(例如,当您使用 StartupMode.HOT
时)将基准测试重置为基本条件。
对于任何其他交互,您可以使用 device
参数,该参数允许您查找 UI 元素、滚动、等待某些内容等。
好的,现在我们已经定义了一个启动基准测试,您将在下一步中运行它。
6. 运行基准测试
在运行基准测试之前,请确保您在 Android Studio 中选择了正确的构建变体
- 选择构建变体面板
- 将活动构建变体更改为benchmark
- 等待 Android Studio 同步
如果您未执行此操作,则基准测试将在运行时失败,并出现错误,指示您不应对 debuggable
应用进行基准测试
java.lang.AssertionError: ERRORS (not suppressed): DEBUGGABLE WARNINGS (suppressed): ERROR: Debuggable Benchmark Benchmark is running with debuggable=true, which drastically reduces runtime performance in order to support debugging features. Run benchmarks with debuggable=false. Debuggable affects execution speed in ways that mean benchmark improvements might not carry over to a real user's experience (or even regress release performance).
您可以使用检测参数 androidx.benchmark.suppressErrors = "DEBUGGABLE"
临时禁止此错误。您可以按照与在 Android 模拟器上运行基准测试步骤中相同的步骤进行操作。
现在,您可以运行基准测试了——就像运行检测测试一样。您可以使用代码旁边的 gutter 图标运行测试函数或整个类。
确保您选择了物理设备,因为在 Android 模拟器上运行基准测试将在运行时失败,并发出警告,指出结果将不准确。虽然从技术上讲您可以在模拟器上运行它,但您实际上是在测量您的宿主机器性能——如果宿主机器负载很重,您的基准测试将表现得更慢,反之亦然。
运行基准测试后,您的应用将被重建,然后运行您的基准测试。基准测试将根据您定义的 iterations
次数多次启动、停止甚至重新安装您的应用。
7.(可选)在 Android 模拟器上运行基准测试
如果您没有物理设备但仍想运行基准测试,可以使用检测参数 androidx.benchmark.suppressErrors = "EMULATOR"
抑制运行时错误
要抑制此错误,请编辑您的运行配置
- 从运行菜单中选择“Edit Configurations...”:
- 在打开的窗口中,选择“Instrumentation arguments”旁边的“选项”图标
- 点击 ➕ 并输入详细信息来添加 Instrumentation extra param
- 点击 OK 确认选择。你应该会在“Instrumentation arguments”行中看到该参数
- 点击 Ok 确认运行配置。
另外,如果你需要将其永久保留在代码库中,可以在 :macrobenchmark
模块的 build.gradle
文件中进行设置。
defaultConfig {
// ...
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = 'EMULATOR'
}
8. 理解启动结果
基准测试运行完成后,它会在 Android Studio 中直接给出结果,如下面的截图所示。
你可以看到,在我们的例子中,Google Pixel 7a 上的启动时间,最小值是 312.5ms,中位数是 337.6ms,最大值是 390.1ms。请注意,在你的设备上运行相同的基准测试时,结果可能会有所不同。结果会受到许多因素的影响,例如:
- 设备性能
- 使用的系统版本
- 后台运行的应用
正因为如此,在同一设备上比较结果非常重要,理想情况下应处于相同状态,否则可能会看到很大的差异。如果你无法保证相同的状态,你可能需要增加 iterations
的数量,以便更好地处理结果中的离群值。
为了便于调查,Macrobenchmark 库会在基准测试执行期间记录系统跟踪。为了方便起见,Android Studio 会将每次迭代和测量的时间标记为系统跟踪的链接,以便你可以轻松打开进行调查。
9.(可选练习)声明应用何时准备就绪可用
Macrobenchmark 可以自动测量应用渲染第一帧的时间(timeToInitialDisplay
)。然而,应用内容通常在第一帧渲染后才加载完成,你可能想知道用户需要等待多久应用才能使用。这被称为完全显示时间 (time to full display) – 应用已完全加载内容,用户可以与它交互。Macrobenchmark 库可以自动检测这个时间点,但你需要修改应用,使用 Activity.reportFullyDrawn()
函数告知何时发生。
示例展示了一个简单的进度条,直到数据加载完毕。因此,你希望等待数据准备就绪且零食列表被布局并绘制完成。让我们修改示例应用并添加 reportFullyDrawn()
调用。
在项目窗格中,打开 .ui.home
包下的 Feed.kt
文件。
在该文件中,找到负责组合零食列表的 SnackCollectionList
Composable 函数。
你需要检查数据是否准备就绪。你知道在内容准备好之前,snackCollections
参数会返回一个空列表,所以你可以使用 ReportDrawnWhen
Composable 函数,它会在谓词为 true 时负责报告。
ReportDrawnWhen { snackCollections.isNotEmpty() }
Box(modifier) {
LazyColumn {
// ...
}
此外,你也可以使用接受 suspend
函数并等待该函数完成的 ReportDrawnAfter{}
Composable 函数。通过这种方式,你可以等待异步加载一些数据,或等待某个动画完成。
完成此步骤后,你需要调整 ExampleStartupBenchmark
以等待内容加载,否则基准测试将在渲染第一帧后完成,并可能遗漏该指标。
当前的启动基准测试仅等待渲染第一帧。等待本身包含在 startActivityAndWait()
函数中。
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.macrobenchmark_codelab",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD,
) {
pressHome()
startActivityAndWait()
// TODO wait until content is ready
}
在我们的例子中,你可以等到内容列表有一些子项,因此添加 wait()
,如下面的代码片段所示
@Test
fun startup() = benchmarkRule.measureRepeated(
//...
) {
pressHome()
startActivityAndWait()
val contentList = device.findObject(By.res("snack_list"))
val searchCondition = Until.hasObject(By.res("snack_collection"))
// Wait until a snack collection item within the list is rendered
contentList.wait(searchCondition, 5_000)
}
解释一下代码片段中发生的情况:
- 我们通过
Modifier.testTag("snack_list")
找到零食列表 - 我们定义了搜索条件,使用
snack_collection
作为要等待的元素 - 我们使用
UiObject2.wait
函数在 UI 对象中等待条件满足,超时时间为 5 秒
现在,你可以再次运行基准测试,库将自动测量 timeToInitialDisplay
和 timeToFullDisplay
,如下面的截图所示:
你可以看到,在我们的例子中,TTID 和 TTFD 之间的差异是 389.8ms。这意味着即使你的用户在 392.9ms 时看到了渲染的第一帧,他们仍然需要额外等待 389.8ms 才能滚动列表。
10. 基准测试帧时间
用户进入应用后,遇到的第二个指标是应用的流畅度。或者用我们的术语来说,应用是否丢帧。为了衡量这一点,我们将使用 FrameTimingMetric
。
假设你想衡量列表项的滚动行为,并且不想测量该场景之前的任何内容。你需要将基准测试分为测量和未测量的交互。为此,我们将使用 setupBlock
lambda 参数。
在未测量的交互(在 setupBlock
中定义)中,我们将启动默认的 Activity;在测量的交互(在 measureBlock
中定义)中,我们将找到 UI 列表元素,滚动列表并等待屏幕渲染内容。如果你没有将交互分为这两部分,你将无法区分应用启动期间生成的帧和列表滚动期间生成的帧。
创建帧时间基准测试
为了实现上述流程,让我们创建一个新的 ScrollBenchmarks
类,其中包含一个 scroll()
测试,该测试将包含滚动帧时间基准测试。首先,创建具有基准测试规则和空测试方法的测试类
@RunWith(AndroidJUnit4::class)
class ScrollBenchmarks {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun scroll() {
// TODO implement scrolling benchmark
}
}
然后,添加带有必需参数的基准测试框架。
@Test
fun scroll() {
benchmarkRule.measureRepeated(
packageName = "com.example.macrobenchmark_codelab",
iterations = 5,
metrics = listOf(FrameTimingMetric()),
setupBlock = {
// TODO Add not measured interactions.
}
) {
// TODO Add interactions to measure list scrolling.
}
}
此基准测试使用与 startup
基准测试相同的参数,但 metrics
参数和 setupBlock
除外。FrameTimingMetric
收集应用生成的帧的时间。
现在,我们来填写 setupBlock
。如前所述,在此 lambda 中,基准测试不会测量交互。你可以使用此块仅打开应用并等待渲染第一帧。
@Test
fun scroll() {
benchmarkRule.measureRepeated(
packageName = "com.example.macrobenchmark_codelab",
iterations = 5,
metrics = listOf(FrameTimingMetric()),
startupMode = StartupMode.COLD,
setupBlock = {
// Start the default activity, but don't measure the frames yet
pressHome()
startActivityAndWait()
}
) {
// TODO Add interactions to measure list scrolling.
}
}
现在,我们来编写 measureBlock
(最后一个 lambda 参数)。首先,由于将项目提交到零食列表是一个异步操作,你应该等待内容准备就绪。
benchmarkRule.measureRepeated(
// ...
) {
val contentList = device.findObject(By.res("snack_list"))
val searchCondition = Until.hasObject(By.res("snack_collection"))
// Wait until a snack collection item within the list is rendered
contentList.wait(searchCondition, 5_000)
// TODO Scroll the list
}
可选地,如果你不关心测量初始布局设置,可以在 setupBlock
中等待内容准备就绪。
接下来,为零食列表设置手势边距。你需要这样做,否则应用可能会触发系统导航,并退出应用而不是滚动内容。
benchmarkRule.measureRepeated(
// ...
) {
val contentList = device.findObject(By.res("snack_list"))
val searchCondition = Until.hasObject(By.res("snack_collection"))
// Wait until a snack collection item within the list is rendered
contentList.wait(searchCondition, 5_000)
// Set gesture margin to avoid triggering system gesture navigation
contentList.setGestureMargin(device.displayWidth / 5)
// TODO Scroll the list
}
最后,你实际上使用 fling()
手势滚动列表(你也可以使用 scroll()
或 swipe()
,具体取决于你想滚动多少和多快),并等待 UI 变为空闲。
benchmarkRule.measureRepeated(
// ...
) {
val contentList = device.findObject(By.res("snack_list"))
val searchCondition = Until.hasObject(By.res("snack_collection"))
// Wait until a snack collection item within the list is rendered
contentList.wait(searchCondition, 5_000)
// Set gesture margin to avoid triggering gesture navigation
contentList.setGestureMargin(device.displayWidth / 5)
// Scroll down the list
contentList.fling(Direction.DOWN)
// Wait for the scroll to finish
device.waitForIdle()
}
该库将在执行定义操作时测量应用生成的帧的时间。
现在你已经准备好运行基准测试了。
运行基准测试
你可以使用与启动基准测试相同的方式运行基准测试。点击测试旁边的 gutter 图标,然后选择 Run ‘scroll()'。
如果你需要有关运行基准测试的更多信息,请查看运行基准测试步骤。
理解结果
FrameTimingMetric
以毫秒为单位输出帧持续时间(frameDurationCpuMs
)的第 50、90、95 和 99 百分位数。在 Android 12(API level 31)及更高版本上,它还会返回帧超出限制的时间(frameOverrunMs
)。该值可能为负,这意味着还有额外时间可以用于生成帧。
从结果中可以看出,在 Google Pixel 7 上创建帧的中位数(P50)是 3.8ms,这比帧时间限制少 6.4ms。但也可能在超过 99 百分位数(P99)时出现了一些跳过的帧,因为这些帧耗时 35.7ms 来生成,这比限制多了 33.2ms。
与应用启动结果类似,你可以点击 iteration
打开基准测试期间记录的系统跟踪,并调查导致最终时间的原因。
11. 恭喜
恭喜,你已成功完成本篇关于使用 Jetpack Macrobenchmark 测量性能的 Codelab!
下一步是什么?
请查阅使用基线配置文件提升应用性能 Codelab。此外,请查看我们的性能示例 Github 仓库,其中包含 Macrobenchmark 和其他性能示例。