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。

💡 小技巧: 如果你发现启动测试结果波动很大,检查一下被测App是否开启了「不保留活动」开发者选项。这个选项会影响Activity的重建逻辑,导致测量不准。

测试布局性能:你的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)
    }
}

注意,这里我手动调用了measurelayout。因为单纯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
布局inflateXML转View树< 10ms
方法执行单次调用耗时视场景而定
GC暂停垃圾回收影响< 5ms

最后说一句:基准测试的结果是「相对值」,不是「绝对值」。不同手机、不同系统版本,结果差异很大。你只需要关注同一个设备上,优化前后的对比。别拿小米和三星的数据去PK,没意义。

Microbenchmark 性能测试体系 Microbenchmark 库 方法性能测试 启动性能测试 布局性能测试 其他专项测试 测量内容 函数执行耗时 算法效率对比 JIT编译影响 测量内容 冷启动时间 温启动/热启动 首帧渲染时间 测量内容 inflate耗时 measure/layout 层级深度影响 测量内容 数据库查询 网络请求 文件IO 关键配置要求 Release模式运行 | minSdk ≥ 24 | 关闭后台应用 | 插电+飞行模式

这张图把Microbenchmark的测试体系梳理清楚了。核心就是那个库,然后向四个方向展开。你不需要一开始就全部掌握,先从「方法性能」入手,跑通一个最简单的测试,再慢慢扩展到启动和布局。

记住:没有测量,就没有优化。你觉得自己代码快,那只是感觉。跑一遍基准测试,数据会告诉你真相。


公众号:蓝海资料掘金营,微信deep3321