1. 准备工作
在此 Codelab 中,您将学习如何使用 Macrobenchmark 库。您将衡量应用启动时间(用户互动度的关键指标)和帧时间(提示应用中可能会出现卡顿的位置)。
所需条件
- Android Studio Dolphin (2021.3.1) 或更高版本
- 了解 Kotlin
- 对 Android 平台测试有基本的了解
- 一台搭载 Android 6(API 级别 23)或更高版本的 Android 实体设备
实践内容
- 将基准化分析模块添加到现有应用
- 衡量应用的启动时间和帧时间
学习内容
- 可靠地衡量应用性能
2. 准备工作
首先,从命令行使用以下命令克隆 GitHub 代码库:
$ git clone 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. Jetpack Macrobenchmark 简介
Jetpack Macrobenchmark 库可以衡量大型最终用户互动(例如启动、与界面的互动和动画)的性能。此库可让您直接控制受测试的性能环境。通过控制应用的编译、启动和停止,您可以直接衡量应用启动、帧时间和所跟踪的代码段。
借助 Jetpack Macrobenchmark,您可以执行以下操作:
- 使用确定的启动模式和滚动速度,对应用进行多次测量
- 对多次运行测试所得的结果取平均值,从而消除性能差异
- 控制应用的编译状态(这是保持性能稳定的一项主要因素)
- 在本地重现 Google Play 商店执行的安装时优化,从而更好地了解实际性能
使用此库的插桩不会直接调用您的应用代码,而是会像用户一样在您的应用中导航(轻触、点击、滑动等)。衡量操作会在这些互动期间在设备上进行。如果您想直接衡量部分应用代码,请改为参阅 Jetpack Microbenchmark。
基准的编写方式与插桩测试一样,只不过您不需要验证应用所处的状态。基准使用 JUnit 语法(@RunWith
、@Rule
、@Test
等),但测试将在单独的进程中运行,以允许对您的应用进行重启或预编译。这样,我们就可以像用户一样运行您的应用,而不会干扰其内部状态。为此,我们使用 UiAutomator
与目标应用进行互动。
示例应用
在此 Codelab 中,您将使用 JetSnack 示例应用。这是一款使用 Jetpack Compose 的虚拟零食订购应用。您无需了解与此应用的架构方式相关的详细信息,即可衡量其性能。您需要了解此应用的行为和界面结构,以便通过基准访问各个界面元素。请运行此应用,然后通过订购您喜欢的零食来熟悉其中的基本界面。
4. 添加 Macrobenchmark 库
Macrobenchmark 需要向您的项目添加新的 Gradle 模块。如需将其添加到项目中,最简便的方法是使用 Android Studio 模块向导。
打开新模块对话框(例如,在 Project 面板中右键点击您的项目或模块,然后依次选择 New > Module)。
从 Templates 窗格中选择 Benchmark,确保已选择 Macrobenchmark 作为基准模块类型,并检查详细信息是否符合预期:
- Target application - 要进行基准测试的应用
- Module name - 基准化分析 Gradle 模块的名称
- Package name - 基准的软件包名称
- Minimum SDK - 至少需要 Android 6(API 级别 23)或更高版本
点击 Finish。
模块向导所做的更改
模块向导会对您的项目进行一些更改。
它会添加一个名为 macrobenchmark
(或您在向导中选择的名称)的 Gradle 模块。此模块使用 com.android.test
插件,后者会告知 Gradle 不要将该模块包含在您的应用中,因此模块中只能包含测试代码(或基准)。
向导还会对您选择的目标应用模块进行更改。具体而言,它会将新的 benchmark
build 类型添加到 :app
模块 build.gradle
,如以下代码段所示:
benchmark {
initWith buildTypes.release
signingConfig signingConfigs.debug
matchingFallbacks = ['release']
debuggable false
}
此 buildType 应尽可能模拟您的 release
buildType。与 release
buildType 的区别在于,需要将 signingConfig
设置为 debug
,这样您便可以在本地构建应用,而无需使用正式版密钥库。
不过,由于 debuggable
标记已停用,因此向导会将 <profileable>
标记添加到 AndroidManifest.xml
,从而让基准能够根据发布性能来分析您的应用。
<application>
<profileable
android:shell="true"
tools:targetApi="q" />
</application>
如需详细了解 <profileable>
的用途,请参阅我们的文档。
向导的最后一项任务是创建一个 Scaffold,以便对启动时间进行基准测试(我们将在下一步骤中使用)。
现在,您可以开始编写基准了。
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、点击界面元素、滚动、滑动等),并且 Macrobenchmark 会收集在此块中定义的metrics
。
如何编写基准操作
Macrobenchmark 将重新安装并重启您的应用。请确保将互动编写为独立于应用状态。Macrobenchmark 提供了多种用于与应用互动的实用函数和形参。
最重要的函数是 startActivityAndWait()
。此函数将启动您的默认 activity,并等待其呈现第一帧,然后再继续按照基准中的说明进行测试。如果您要启动其他 activity 或调整启动 intent,可以使用可选的 intent
或 block
形参来实现此目的。
另一个实用函数是 pressHome()
。如果您在每次迭代时未终止应用(例如,当您使用 StartupMode.HOT
时),可以使用此函数将基准重置为基本条件。
对于任何其他互动,您可以使用 device
形参,通过此形参,您可以查找界面元素、滚动、等待某些内容或执行其他操作。
好了,现在我们已经定义了一个启动基准,您将在下一步骤中运行此基准。
6. 运行基准
在运行基准测试之前,请确保您在 Android Studio 中选择了正确的 build 变体:
- 选择 Build Variants 面板
- 将 Active Build Variant 更改为 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 模拟器上运行基准相同的步骤。
现在,您可以运行基准了,方法与运行插桩测试相同。您可以运行测试函数或整个类(使用其旁边的边线图标)。
请确保您已选择实体设备,因为在 Android 模拟器上运行基准会在运行时失败并显示一条警告,提示说测试会显示错误结果。虽然从技术层面来说,您可以在模拟器上运行,但从根本上说,这时您衡量的是宿主机的性能。如果宿主机负载较高,基准测试就会进展缓慢,反之亦然。
一旦您运行基准,您的应用就会重新构建,然后运行基准。这些基准将根据您定义的 iterations
多次启动、停止甚至重新安装您的应用。
7. (可选)在 Android 模拟器上运行基准
如果您没有实体设备,并且仍想运行基准,可以使用插桩实参 androidx.benchmark.suppressErrors = "EMULATOR"
来抑制运行时错误
如需抑制错误,请修改运行配置:
- 从运行菜单中选择“Edit Configurations…”:
- 在打开的窗口中,选择“Instrumentation arguments”旁边的“选项”图标
- 点击 ➕ 并输入详细信息,以添加插桩额外形参
- 点击 OK 确认选择。您应该会在“Instrumentation arguments”行中看到相应实参
- 点击 Ok 确认运行配置。
或者,如果您需要将它永久保留在代码库中,可以在 :macrobenchmark
模块的 build.gradle
中执行相关操作:
defaultConfig {
// ...
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = 'EMULATOR'
}
8. 了解启动测试结果
基准运行完毕后,它将直接在 Android Studio 中提供测试结果,如以下屏幕截图所示:
您可以看到,在本例中,对于 Google Pixel 7 上的启动时间,最小值为 294.8 毫秒,中位数为 301.5 毫秒,最大值为 314.8 毫秒。请注意,在您的设备上运行相同的基准时,结果可能会有所不同。结果可能会受许多因素的影响,例如:
- 设备性能如何
- 设备使用的是哪个系统版本
- 哪些应用在后台运行
因此,请务必比较在同一设备上测得的结果,最好是在相同状态下测得的结果,否则您可能会发现测试结果存在巨大差异。如果您无法保证状态相同,可能需要增加 iterations
的数量,以便正确处理结果离群值。
为便于调查,Macrobenchmark 库会在执行基准测试期间记录系统跟踪数据。为方便起见,Android Studio 会将每次迭代和衡量的时间标记为指向相应系统跟踪记录的链接,以便您轻松打开进行研究。
9. (可选练习)声明您的应用何时可以使用
Macrobenchmark 可以自动衡量您的应用呈现第一帧所需的时间 (timeToInitialDisplay
)。不过,直到第一帧呈现之后,应用内容仍未完成加载的情况也很常见,而您可能想要了解用户需要等待多长时间才能使用应用。此等待时间称为完全显示所用时间,即应用内容已全部加载,且用户可以与应用互动所需的时间。Macrobenchmark 库可以自动检测此时间,但您需要使用 Activity.reportFullyDrawn()
函数来调整应用,使其向 Macrobenchmark 库传达完全显示发生的时间。
此示例会显示一个简单的进度条,直到数据加载完成为止。因此,您需要等待,直到数据准备就绪且系统列出并绘制零食列表。我们来调整示例应用并添加 reportFullyDrawn()
调用。
从 Project 窗格打开软件包 .ui.home
中的 Feed.kt
文件。
在此文件中,找到负责编写零食列表的 SnackCollectionList
可组合项。
您需要检查数据是否准备就绪。您知道,在内容准备就绪之前,snackCollections
形参会为您提供一个空列表,因此您可以使用 ReportDrawnWhen
可组合项,以便在谓词为 true 时处理报告。
ReportDrawnWhen { snackCollections.isNotEmpty() }
Box(modifier) {
LazyColumn {
// ...
}
或者,您也可以使用接受 suspend
函数的 ReportDrawnAfter{}
可组合项,并等待此函数完成运行。这样一来,您就可以等待一些数据异步加载,或等待动画播放完毕。
完成后,您需要调整 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
函数在界面对象中等待此条件,并将超时时间设置为 5 秒。
现在,您可以再次运行基准,此时该库会自动衡量 timeToInitialDisplay
和 timeToFullDisplay
,如以下屏幕截图所示:
您可以看到,在本例中,TTID 与 TTFD 的差值为 413 毫秒。这意味着,即使第一帧只需 319.4 毫秒就能呈现给用户,但用户还需再等待 413 毫秒才能滚动列表。
10. 对帧时间进行基准测试
在用户进入您的应用后,他们遇到的第二个指标就是应用的流畅度。用我们的术语说,就是应用是否会丢帧。为了衡量这一指标,我们将使用 FrameTimingMetric
。
假设您想要衡量项列表的滚动行为,并且不想衡量滚动情景发生前的任何对象。您需要将基准测试拆分为衡量的互动和不衡量的互动。为此,我们将使用 setupBlock
lambda 形参。
在不衡量的互动(在 setupBlock
中定义)中,我们将启动默认 activity;在衡量的互动(在 measureBlock
中定义)中,我们将找到界面列表元素、滚动列表并等待屏幕呈现内容。如果您未将互动分为两部分,则将无法区分在应用启动期间生成的帧和列表滚动期间生成的帧。
创建帧时间基准
为了实现上述流程,我们来创建一个新的 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()),
startupMode = StartupMode.COLD,
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()
,具体取决于您想要滚动的幅度和速度)来实际滚动列表,并等待界面变为空闲状态。
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()
}
该库将在执行定义的操作时衡量应用生成的帧的时间。
现在,您的基准可以运行了。
运行基准
运行基准的方式与启动基准相同。点击测试旁边的边线图标,然后选择 Run 'scroll()'。
如需详细了解如何运行基准,请参阅运行基准步骤。
了解测试结果
FrameTimingMetric
会在第 50、第 90、第 95 和第 99 百分位以毫秒 (frameDurationCpuMs
) 为单位输出帧时长。在 Android 12(API 级别 31)及更高版本中,它还会返回帧超出限制的时长 (frameOverrunMs
)。此值可以为负数,这意味着还有额外的时间可用于生成帧。
从结果中可以看出,在 Google Pixel 3 上创建帧的中间值 (P50) 为 3.8 毫秒,低于帧时间限制 6.4 毫秒。此外,在百分位数高于 99 (P99) 的帧中可能存在一些跳过的帧,这是因为生成帧需要 35.7 毫秒,超出了上限 33.2 毫秒。
与应用启动测试结果一样,您可以点击 iteration
以打开在基准测试期间记录的系统跟踪数据,并调查所得时间是在哪些因素的作用下产生的。
11. 恭喜
恭喜!您已成功完成此 Codelab,了解了如何使用 Jetpack Macrobenchmark 来衡量性能。
后续操作
查看使用基准配置文件提升应用性能 Codelab。此外,请查看性能示例 GitHub 代码库,其中包含 Macrobenchmark 和其他性能示例。