22、性能测试与基准测试:Microbenchmark库使用、配置基准测试、测试方法性能、测试启动性能、测试布局性能
性能测试,说白了就是给你的App做一次「体检」。
我见过太多项目,功能跑得飞起,一到真机就卡成PPT。用户可不会管你代码写得有多优雅,他们只关心「点一下,等多久」。所以,性能测试不是锦上添花,而是保命底线。
今天我们来聊聊Android上的基准测试利器——Microbenchmark库。它来自Jetpack,专门用来测量代码片段的执行时间。注意,它不是用来测UI流畅度的,那是另一回事。
Microbenchmark库的引入与配置
首先,你得在项目中引入这个库。我建议单独建一个:benchmark模块,别跟主模块混在一起。为什么?因为基准测试需要特定的构建配置,混在一起容易出幺蛾子。
在项目根目录的settings.gradle.kts里加上:
include(":benchmark")
然后在benchmark/build.gradle.kts里配置:
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("androidx.benchmark") // 关键插件
}
android {
namespace = "com.example.benchmark"
compileSdk = 34
defaultConfig {
minSdk = 24
targetSdk = 34
testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
dependencies {
implementation(project(":app")) // 依赖被测模块
implementation("androidx.benchmark:benchmark-junit4:1.2.4")
androidTestImplementation("junit:junit:4.13.2")
}
嗯,这里要注意:基准测试必须在Release模式下运行。Debug模式会附加调试器,严重影响时间测量。我曾经有个同事,Debug模式下测出来的结果比Release慢了10倍,他还以为是自己代码写得烂……
minSdkVersion不能低于24。因为底层依赖了一些API,低于24会直接崩溃。别问我怎么知道的,踩过坑。
编写第一个基准测试
配置好了,我们来写一个最简单的测试。假设你要测一个字符串拼接的性能:
@RunWith(AndroidJUnit4::class)
class StringBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun stringConcatenation() {
benchmarkRule.measureRepeated {
var result = ""
for (i in 0 until 100) {
result += i.toString()
}
}
}
}
这段代码的核心就是benchmarkRule.measureRepeated { ... }。它会自动重复执行你传入的代码块,然后统计出最小时间、中位数、最大时间等指标。你不需要手动写循环计时,库帮你搞定了。
我个人习惯把被测代码提取成一个单独的函数,这样测试方法看起来更清爽:
@Test
fun stringBuilderBenchmark() {
benchmarkRule.measureRepeated {
stringBuilderConcat()
}
}
private fun stringBuilderConcat(): String {
val sb = StringBuilder()
for (i in 0 until 100) {
sb.append(i)
}
return sb.toString()
}
你看,这样测试方法就只关注「测什么」,而「怎么测」被封装起来了。维护起来也方便。
测试方法性能:不止是跑得快
方法性能测试,说白了就是看一个函数执行一次要多久。但这里有个坑:JIT编译。Android的ART运行时会在方法被多次调用后,将其编译为机器码。所以第一次调用和第一百次调用的速度可能天差地别。
Microbenchmark库已经考虑到了这一点。它会自动进行「预热」——先跑几轮让JIT生效,然后再开始正式测量。你不需要手动处理。
但有一点要注意:被测代码里不要有分支预测或缓存的干扰。比如你测一个排序算法,第一次排乱序,第二次排有序,结果可能差很多。我建议在测试方法里每次都生成新的随机数据:
@Test
fun quickSortBenchmark() {
benchmarkRule.measureRepeated {
val data = (1..1000).shuffled().toIntArray()
quickSort(data)
}
}
这样每次执行的环境都差不多,结果才可信。
测试启动性能:App冷启动到底有多慢?
App启动速度是用户的第一印象。启动慢,用户可能直接卸载。Microbenchmark库提供了一个专门的StartupBenchmark规则来测这个。
首先,在benchmark模块的AndroidManifest.xml里声明:
<instrumentation
android:name="androidx.benchmark.junit4.AndroidBenchmarkRunner"
android:targetPackage="com.example.app" />
然后写测试:
@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun startup() {
benchmarkRule.measureRepeated(
metrics = listOf(StartupTimingMetric()),
iterations = 5,
warmupIterations = 2
) {
val intent = Intent()
intent.setClassName("com.example.app", "com.example.app.MainActivity")
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
// 等待Activity完全显示
val idle = context.getSystemService()?.isInteractive
}
}
}
这里有个关键点:StartupTimingMetric()会测量从进程启动到第一帧渲染的时间。它会把时间拆分成「冷启动」「温启动」「热启动」三种场景。冷启动最慢,因为要创建进程、加载类、初始化Application。
测试布局性能:你的XML渲染快吗?
布局性能,说白了就是XML转成View树要多久。复杂的嵌套布局、过深的层级,都会拖慢渲染速度。Microbenchmark库可以帮你量化这个问题。
测试布局性能,通常用LayoutBenchmark或者自己写一个简单的测试:
@Test
fun complexLayoutBenchmark() {
benchmarkRule.measureRepeated {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.activity_complex, null, false)
// 强制测量和布局
view.measure(
View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY)
)
view.layout(0, 0, 1080, 1920)
}
}
注意,这里我手动调用了measure和layout。因为单纯inflate并不会触发布局计算,而布局计算才是真正耗时的部分。你想想看,一个RelativeLayout里嵌套了5层LinearLayout,光计算约束就要花多少时间?
我曾经优化过一个项目,主界面布局层级有11层深。用这个测试一跑,inflate+measure耗时接近80ms。后来改成了ConstraintLayout,层级压到3层,耗时直接降到15ms。用户反馈「App变快了」,其实就是布局渲染的功劳。
结果解读与避坑
跑完测试,你会看到类似这样的输出:
Benchmark Mode Cnt Score Error Units
StringBenchmark.stringConcat avgt 10 12.345 ± 0.678 us/op
这个us/op表示每次操作耗时多少微秒。数值越小越好。但要注意,这个「误差」是统计出来的标准差,不是绝对精度。如果误差超过10%,说明你的测试环境不稳定——可能是后台有App在跑,或者CPU降频了。
我建议每次跑基准测试前,先关掉所有后台应用,把手机插上电源,并且开启飞行模式。这样能最大程度减少干扰。
| 指标 | 含义 | 理想范围 |
|---|---|---|
| 冷启动时间 | 进程创建到首帧 | < 500ms |
| 布局inflate | XML转View树 | < 10ms |
| 方法执行 | 单次调用耗时 | 视场景而定 |
| GC暂停 | 垃圾回收影响 | < 5ms |
最后说一句:基准测试的结果是「相对值」,不是「绝对值」。不同手机、不同系统版本,结果差异很大。你只需要关注同一个设备上,优化前后的对比。别拿小米和三星的数据去PK,没意义。
这张图把Microbenchmark的测试体系梳理清楚了。核心就是那个库,然后向四个方向展开。你不需要一开始就全部掌握,先从「方法性能」入手,跑通一个最简单的测试,再慢慢扩展到启动和布局。
记住:没有测量,就没有优化。你觉得自己代码快,那只是感觉。跑一遍基准测试,数据会告诉你真相。
公众号:蓝海资料掘金营,微信deep3321