1. 开始之前
在本 Codelab 中,您将学习如何使用 macrobenchmark 库。您将测量应用启动时间(这是用户参与度的关键指标)和帧时间(暗示应用中可能出现卡顿的位置)。
您需要什么
- Android Studio Dolphin (2021.3.1) 或更高版本
- 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 模块向导。
打开新的模块对话框(例如,右键点击**Project** 面板中的项目或模块,然后选择**New > Module**)。
从**Templates** 面板中选择**Benchmark**,确保选择**Macrobenchmark** 作为 Benchmark 模块类型,并检查详细信息是否符合预期。
- **Target application** – 将要进行基准测试的应用程序
- **Module name** – 基准测试 Gradle 模块的名称
- **Package name** – 基准测试的包名
- **Minimum SDK** – 至少需要 Android 6(API 级别 23)或更高版本。
点击**Finish**。
模块向导所做的更改
模块向导对您的项目进行了一些更改。
它添加了一个名为macrobenchmark
(或您在向导中选择的名称)的 Gradle 模块。此模块使用com.android.test
插件,该插件告诉 Gradle 不要将其包含在您的应用程序中,因此它只能包含测试代码(或基准测试)。
向导还会对您选择的目标应用程序模块进行更改。具体来说,它会向:app
模块的build.gradle
中添加新的benchmark
build type,如下面的代码片段所示。
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>
功能的更多信息,请查看我们的文档。
向导做的最后一件事是创建用于基准测试启动时间的脚手架(我们将在下一步中使用)。
现在您已准备好开始编写基准测试了。
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 中选择了正确的构建变体。
- 选择**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).
您可以使用 instrumentation 参数androidx.benchmark.suppressErrors = "DEBUGGABLE"
暂时抑制此错误。您可以按照在 Android 模拟器上运行基准测试步骤中的相同步骤操作。
现在,您可以运行基准测试了——就像运行已测工具测试一样。您可以使用它旁边的代码行图标运行测试函数或整个类。
确保已选择物理设备,因为在 Android 模拟器上运行基准测试将在运行时失败,并显示警告信息,指出这将给出不正确的结果。虽然从技术上讲您可以在模拟器上运行它,但您基本上是在测量主机计算机的性能——如果主机计算机负载过重,您的基准测试将运行得更慢,反之亦然。
运行基准测试后,您的应用程序将被重新构建,然后它将运行您的基准测试。根据您定义的iterations
,基准测试将启动、停止甚至重新安装您的应用程序几次。
7.(可选)在 Android 模拟器上运行基准测试
如果您没有物理设备,但仍想运行基准测试,则可以使用 instrumentation 参数androidx.benchmark.suppressErrors = "EMULATOR"
抑制运行时错误。
要抑制错误,请编辑您的运行配置。
- 从运行菜单中选择“Edit Configurations...”:
- 在打开的窗口中,选择“选项”图标 ,位于“Instrumentation arguments”旁边
- 通过点击➕并输入详细信息来添加 Instrumentation 额外参数
- 点击**确定**确认选择。您应该在“Instrumentation arguments”行中看到该参数
- 点击**确定**确认运行配置。
或者,如果您需要将其永久保存在您的代码库中,您可以从build.gradle
文件中的:macrobenchmark
模块中执行此操作。
defaultConfig {
// ...
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = 'EMULATOR'
}
8. 了解启动结果
基准测试运行完成后,它将直接在 Android Studio 中为您提供结果,如下面的屏幕截图所示
您可以看到,在我们的例子中,Google Pixel 7 上的启动时间,最小值为**294.8ms**,中位数为**301.5ms**,最大值为**314.8ms**。请注意,在您的设备上,运行相同的基准测试时可能会得到不同的结果。结果可能会受到许多因素的影响,例如
- 设备的性能如何
- 它使用什么系统版本
- 后台运行哪些应用
因此,在同一设备上比较结果非常重要,理想情况下应处于相同状态,否则您可能会看到很大的差异。如果您无法保证相同的状态,您可能需要增加iterations
的数量以正确处理结果异常值。
为了便于调查,Macrobenchmark 库在基准测试执行期间记录系统跟踪。为了方便起见,Android Studio 将每次迭代和测量的计时标记为指向系统跟踪的链接,以便您可以轻松地打开它进行调查。
9. (可选练习)声明您的应用何时可以使用
Macrobenchmark 可以自动测量应用渲染第一帧的时间(timeToInitialDisplay
)。但是,您的应用内容通常在渲染第一帧后才会完成加载,您可能想知道用户必须等待多长时间才能使用该应用。这称为完整显示时间——应用已完全加载内容,用户可以与之交互。Macrobenchmark 库可以自动检测此计时,但您需要调整您的应用以使用Activity.reportFullyDrawn()
函数告知何时发生。
示例显示了一个简单的进度条,直到数据加载完成,因此您需要等到数据准备就绪并且零食列表已布局并绘制。让我们调整示例应用程序并添加reportFullyDrawn()
调用。
在**Project**窗格中打开.ui.home
包中的Feed.kt
文件。
在该文件中,找到负责组合零食列表的SnackCollectionList
可组合函数。
您需要检查数据是否已准备就绪。您知道,在内容准备就绪之前,您会从snackCollections
参数中获得一个空列表,因此您可以使用ReportDrawnWhen
可组合函数,该函数将在谓词为真时处理报告。
ReportDrawnWhen { snackCollections.isNotEmpty() }
Box(modifier) {
LazyColumn {
// ...
}
或者,您也可以使用ReportDrawnAfter{}
可组合函数,该函数接受suspend
函数并等待此函数完成。这样,您可以等待一些数据异步加载或某些动画完成。
完成此操作后,您需要调整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 之间的差异为**413ms**。这意味着,即使您的用户在**319.4ms**内看到第一个渲染的帧,他们也需要额外等待**413ms**才能滚动列表。
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()),
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()
)并等待 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()
}
库将在执行定义的操作时测量我们的应用程序生成的帧的时间。
现在,基准测试已准备好运行。
运行基准测试
您可以像运行启动基准测试一样运行该基准测试。单击测试旁边的装订线图标,然后选择“运行‘scroll()’”。
如果您需要有关运行基准测试的更多信息,请查看运行基准测试步骤。
理解结果
FrameTimingMetric
以毫秒为单位输出帧的持续时间(frameDurationCpuMs
),分别位于第 50、90、95 和 99 个百分位数。在 Android 12(API 级别 31)及更高版本上,它还会返回帧超出限制的时间(frameOverrunMs
)。该值可以为负数,这意味着还有额外的时间来生成帧。
您可以从结果中看到,在 Google Pixel 7 上创建帧的中位数(P50)为3.8 毫秒,比帧时间限制低 6.4 毫秒。但是,在超过 99 个百分位数(P99)的百分位数中,可能有一些帧被跳过,因为帧生成需要35.7 毫秒,比限制高 33.2 毫秒。
与应用程序启动结果类似,您可以单击iteration
以打开在基准测试期间记录的系统跟踪,并调查导致最终时间的原因。
11. 恭喜
恭喜您已成功完成此关于使用 Jetpack Macrobenchmark 测量性能的 codelab!
下一步是什么?
查看使用基线配置文件改进应用程序性能 codelab。此外,请查看我们的性能示例 Github 存储库,其中包含 Macrobenchmark 和其他性能示例。