18、性能测试:Macrobenchmark入门、启动性能、帧率测试
性能测试这件事,说实话,在Compose时代比以前更重要了。为什么?因为Compose的渲染机制是声明式的,一个重组链条没写好,可能整个列表滑动都卡顿。我以前在做一个电商项目时,就因为一个LazyColumn里嵌套了太多Modifier,导致帧率直接掉到30fps以下。用户反馈说「滑动像在泥里走路」——嗯,这个比喻我记到现在。
所以这一章,我们来聊聊Android官方提供的性能测试利器:Macrobenchmark。它能帮你量化启动耗时、帧率稳定性,甚至能捕捉到ANR的苗头。
18.1 Macrobenchmark是什么?
Macrobenchmark是Jetpack里专门用来做「宏观性能测试」的库。它不像Microbenchmark那样测单个函数的执行时间,而是直接启动你的App,或者模拟用户操作,然后采集系统层面的性能指标。
说白了,它就是一个自动化黑盒测试工具。你告诉它「启动App」,它就帮你启动,然后告诉你「冷启动花了多少毫秒」「首帧渲染用了多久」「滑动时有没有掉帧」。
我个人习惯把Macrobenchmark放在一个独立的:macrobenchmark模块里,和单元测试、UI测试分开。这样CI流水线里可以单独跑性能测试,不会影响其他测试的执行时间。
18.2 环境配置
首先,你需要在项目中添加Macrobenchmark的依赖。注意,它需要配合androidx.benchmark一起使用。
// 在项目根目录的 build.gradle 中
buildscript {
dependencies {
classpath "androidx.benchmark:benchmark-gradle-plugin:1.2.0"
}
}
// 在 app 模块的 build.gradle 中
android {
// 必须开启 profileable 模式
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
benchmark {
initWith buildTypes.release
signingConfig signingConfigs.debug
matchingFallbacks = ['release']
debuggable false
}
}
}
// 在 macrobenchmark 模块的 build.gradle 中
dependencies {
implementation 'androidx.benchmark:benchmark-macro-junit4:1.2.0'
implementation 'androidx.test.ext:junit:1.1.5'
implementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
profileable 为 true 的构建类型上运行。如果你用 debug 类型跑,结果会不准确,因为 debug 模式下的性能开销比 release 大很多。
18.3 启动性能测试
启动性能是用户对App的第一印象。我见过一个App冷启动花了8秒,用户直接卸载了。所以这块必须测。
Macrobenchmark提供了StartupMode来区分冷启动、温启动和热启动。我们一般重点关注冷启动。
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startupCold() = benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD,
setupBlock = {
// 每次启动前清空应用数据,模拟首次安装后的启动
pressHome()
}
) {
// 启动App
startActivityAndWait()
// 等待首帧渲染完成
device.wait(Until.hasObject(By.res("main_content")), 5_000)
}
}
这段代码做了几件事:
- 使用
StartupTimingMetric采集启动时间 - 迭代5次取平均值,减少随机波动
- 在
setupBlock里按Home键,确保下次启动是冷启动 - 用
device.wait等待关键UI元素出现,确保首帧已经渲染
跑完之后,你会得到类似这样的结果:
| 指标 | 含义 | 参考值 |
|---|---|---|
| timeToInitialDisplayMs | 从点击图标到首帧渲染的时间 | < 500ms |
| timeToFullDisplayMs | 从点击图标到内容完全加载的时间 | < 2000ms |
timeToInitialDisplayMs 超过1秒,建议检查 Application 的 onCreate() 里有没有做耗时操作。我曾经把一个图片加载库的初始化从 Application 移到了 Activity 的 onCreate() 里,启动时间直接降了300ms。
18.4 帧率测试
帧率测试,说白了就是看你的App在滑动、动画、列表滚动时会不会卡顿。Macrobenchmark提供了FrameTimingMetric来采集每一帧的渲染时间。
@Test
fun scrollListFrames() = benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(FrameTimingMetric()),
iterations = 5,
startupMode = StartupMode.WARM,
setupBlock = {
// 先启动App并导航到列表页面
startActivityAndWait()
device.wait(Until.hasObject(By.res("list_recycler")), 5_000)
}
) {
// 模拟滑动操作
val list = device.findObject(By.res("list_recycler"))
list.setGestureMargin(device.displayWidth / 2)
// 向下滑动
list.fling(Direction.DOWN)
device.waitForIdle()
// 向上滑动
list.fling(Direction.UP)
device.waitForIdle()
}
跑完之后,你会得到帧率分布数据:
| 指标 | 含义 | 参考值 |
|---|---|---|
| frameDurationCpuMs | CPU处理帧的时间(毫秒) | < 16ms(60fps) |
| frameDurationGpuMs | GPU渲染帧的时间(毫秒) | < 16ms |
| frameOverrunMs | 帧超时时间(掉帧量) | 0ms 为最佳 |
frameOverrunMs 的平均值超过 5ms,说明你的列表在滑动时存在明显的掉帧。这时候你需要检查 LazyColumn 的 item 里有没有复杂的重组逻辑,或者有没有不必要的 remember 缺失。
18.5 知识体系图
下面这张图总结了Macrobenchmark的核心流程和指标关系:
18.6 避坑指南
我在实际项目中踩过不少坑,这里分享几个最常见的:
- 不要在模拟器上跑Macrobenchmark——模拟器的CPU调度和真机完全不同,结果没有参考价值。我建议用Pixel设备或者至少是主流机型。
- 迭代次数不要少于5次——性能数据波动很大,3次以下的结果可能完全不可信。我曾经有一次只跑了3次,结果平均值比5次多了200ms,后来发现是第一次迭代时系统还在做JIT编译。
- 注意系统负载——跑测试前关掉后台App、关闭通知、关闭省电模式。否则你的测试结果里会混入系统服务的干扰。
- 使用
profileable构建类型——这个前面提过,但值得再说一遍。用debug类型跑出来的数据会偏大,因为debug模式下Compose的优化是关闭的。
setupBlock 里加一个 device.waitForIdle() 等待系统空闲。有时候前一个测试的动画残留会影响下一个测试的初始状态。
18.7 总结
Macrobenchmark是Android性能测试的「最后一公里」。它帮你把启动时间、帧率这些用户能直接感知的指标量化出来。我个人习惯在每次发版前跑一遍Macrobenchmark,然后把结果和上一次的基线对比。如果启动时间增加了超过10%,我会立刻回查代码。
嗯,性能测试这件事,说白了就是「用数据说话」。你觉得自己优化得很好?跑一遍Macrobenchmark,数据会告诉你真相。
公众号:蓝海资料掘金营,微信deep3321