编写 Android Microbenchmark:配置、计时范围与结果检查

Microbenchmark 用来测量应用中的小段代码。临时测一次,可以在本地工作副本中采用快速配置;准备长期跟踪性能时,应建立独立的基准测试模块,使测试配置与日常开发分开。

快速开始

快速配置不要求先把业务代码拆到其他模块。不过,为了使测量更接近发布构建,配置会关闭应用调试功能。请只在本地工作副本试用这些改动,不要把这一组临时配置提交到版本控制系统。

先在模块的 build.gradle.kts 或 build.gradle 中增加依赖。下面保留原文示例使用的 1.2.4,它是本例的固定版本,不表示当前最新版。

Kotlin DSL:

dependencies {
    implementation("androidx.benchmark:benchmark-junit4:1.2.4")
}

Groovy:

dependencies {
    implementation 'androidx.benchmark:benchmark-junit4:1.2.4'
}

此处使用 implementation,而不是 androidTestImplementation。按照这套快速配置,如果只使用后者,基准测试库的 manifest 不会合并到应用的 manifest,测试将无法正常运行。

接着将 debug 构建类型设为不可调试。示例中的 ... 表示其他已有配置,不能作为可执行语句照抄。

Kotlin DSL:

android {
    ...
    buildTypes {
        debug {
            isDebuggable = false
        }
    }
}

Groovy:

android {
    ...
    buildTypes {
        debug {
            debuggable false
        }
    }
}

把插桩测试运行器改为 AndroidBenchmarkRunner:

android {
    ...
    defaultConfig {
        testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
    }
}

Groovy 的对应写法:

android {
    ...
    defaultConfig {
        testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner"
    }
}

在 androidTest 目录的测试文件中创建 BenchmarkRule。测试仍然是标准 Android 插桩测试,下面用 doSomeWork() 代表实际待测工作,项目需要自行定义这个函数并补齐相应导入。

Kotlin:

@RunWith(AndroidJUnit4::class)
class SampleBenchmark {
    @get:Rule
    val benchmarkRule = BenchmarkRule()

    @Test
    fun benchmarkSomeWork() {
        benchmarkRule.measureRepeated {
            doSomeWork()
        }
    }
}

Java:

@RunWith(AndroidJUnit4.class)
class SampleBenchmark {
    @Rule
    public BenchmarkRule benchmarkRule = new BenchmarkRule();

    @Test
    public void benchmarkSomeWork() {
        final BenchmarkState state = benchmarkRule.getState();
        while (state.keepRunning()) {
            doSomeWork();
        }
    }
}

原文快速开始的 Java 示例使用 BenchmarkRuleKt.measureRepeated(...),但展示的调用缺少规则接收者,括号结构也多了一层闭合。本处改用原文后文已经给出的 BenchmarkState 循环,避免把那个不完整片段直接当作可编译示例。该调整依据 BenchmarkState 的官方接口说明。

为持续测试建立独立模块

长期使用时,把基准测试放入专门模块,让 debuggable=false 等设置与普通测试分开。如果普通测试也使用不可调试的模块,调试器和相关性能分析工具将无法照常附加到那些测试上。

Microbenchmark 会直接调用待测代码。因此,可以把可测量的代码抽到一个公共 Gradle 模块,再由应用和测试模块分别依赖它。原文模块关系图表达的是以下结构:

模块 职责与依赖
:benchmarkable 保存可直接调用的待测代码
:app 应用模块,依赖 :benchmarkable
:microbenchmark 基准测试模块,通过测试依赖引用 :benchmarkable

Android Studio 的模块向导可以生成已经配置好的基准测试模块,包括基准测试目录和 debuggable=false。在 Project 面板中右键项目或模块,依次选择 New → Module,在模板面板选择 Benchmark,类型选择 Microbenchmark,将模块命名为 microbenchmark,最后点击 Finish。原文截图展示的是 Android Studio Bumblebee 的向导;界面布局可能随版本变化,上述模块类型和用途才是需要核对的信息。

如果需要在库模块的基准测试中启用代码缩减,将 android.buildTypes.release.androidTest.enableMinification 设为 true。这一能力要求 Android Gradle Plugin 8.3 或更高版本。

创建模块后,在其构建文件中添加对待测模块的测试依赖。实际模块名可以与示例不同。

Kotlin DSL:

dependencies {
    // The module name might be different.
    androidTestImplementation(project(":benchmarkable"))
}

Groovy:

dependencies {
    // The module name might be different.
    androidTestImplementation project(':benchmarkable')
}

创建 Microbenchmark 测试类

使用基准测试库提供的 BenchmarkRule。测量 Activity 时,可以配合 ActivityScenario 或 ActivityScenarioRule;需要在 UI 线程运行的被测代码可使用 @UiThreadTest。

Kotlin 版本:

@RunWith(AndroidJUnit4::class)
class SampleBenchmark {
    @get:Rule
    val benchmarkRule = BenchmarkRule()

    @Test
    fun benchmarkSomeWork() {
        benchmarkRule.measureRepeated {
            doSomeWork()
        }
    }
}

Java 版本:

@RunWith(AndroidJUnit4.class)
class SampleBenchmark {
    @Rule
    public BenchmarkRule benchmarkRule = new BenchmarkRule();

    @Test
    public void benchmarkSomeWork() {
        final BenchmarkState state = benchmarkRule.getState();
        while (state.keepRunning()) {
            doSomeWork();
        }
    }
}

Kotlin 使用 measureRepeated,Java 示例通过 getState() 取得 BenchmarkState 并在 keepRunning() 循环中执行待测工作。迭代次数由库管理,不应擅自把它替换成任意固定次数的循环。

把准备工作移出计时区间

有些工作每轮都必须执行,却不属于想要测量的算法。例如就地排序会改变输入数组,下一轮应先恢复原始数据。Kotlin 可以通过 runWithTimingDisabled 暂停计时,在复制完成后继续测量排序。

以下保留原文的排序示例片段。它们依赖完整测试类中的字段、导入、排序实现和扩展函数;其中 ...、listToSort 及 SortingAlgorithms 不构成独立程序。完整上下文见官方 SortingBenchmarks.kt。

// using random with the same seed, so that it generates the same data every run
private val random = Random(0)

// create the array once and just copy it in benchmarks
private val unsorted = IntArray(10_000) { random.nextInt() }

@Test
fun benchmark_quickSort() {
    // ...
    benchmarkRule.measureRepeated {
        // copy the array with timing disabled to measure only the algorithm itself
        listToSort = runWithTimingDisabled { unsorted.copyOf() }

        // sort the array in place and measure how long it takes
        SortingAlgorithms.quickSort(listToSort)
    }

    // assert only once not to add overhead to the benchmarks
    assertTrue(listToSort.isSorted)
}

Java 通过 pauseTiming() 和 resumeTiming() 控制同一范围:

private final int[] unsorted = new int[10000];

public SampleBenchmark() {
    // Use random with the same seed, so that it generates the same data every
    // run.
    Random random = new Random(0);

    // Create the array once and copy it in benchmarks.
    Arrays.setAll(unsorted, (index) -> random.nextInt());
}

@Test
public void benchmark_quickSort() {
    final BenchmarkState state = benchmarkRule.getState();
    int[] listToSort = new int[0];

    while (state.keepRunning()) {

        // Copy the array with timing disabled to measure only the algorithm
        // itself.
        state.pauseTiming();
        listToSort = Arrays.copyOf(unsorted, 10000);
        state.resumeTiming();

        // Sort the array in place and measure how long it takes.
        SortingAlgorithms.quickSort(listToSort);
    }
    // Assert only once, not to add overhead to the benchmarks.
    assertTrue(SortingAlgorithmsKt.isSorted(listToSort));
}

固定随机种子使每次测试生成相同的输入;每一轮复制数组,使排序算法不会反复处理已经排好序的数据。正确性断言放在循环外,对最后一次结果检查一次,避免在每轮测量中加入不必要的断言开销。

计时区间与暂停计时区间都应尽量精简。暂停计时只是从所报告的被测时长中排除准备工作,准备工作仍会执行并增加整次测试所需的时间。

运行测试

在 Android Studio 中,像执行其他 @Test 一样,点击测试类或方法旁编辑器边栏的运行按钮。原文运行截图的关键位置就是这个边栏入口。

也可以从项目目录执行指定模块的全部连接设备测试:

./gradlew benchmark:connectedCheck

仅运行一个测试方法:

./gradlew benchmark:connectedCheck -P android.testInstrumentationRunnerArguments.class=com.example.benchmark.SampleBenchmark#benchmarkSomeWork

这里沿用原文的 benchmark 模块名。如果前面采用向导默认示例名 microbenchmark,需要相应替换命令中的模块部分。包名、类名和方法名也必须与项目实际代码一致。

查看测量结果

成功执行后,Android Studio 会直接显示测量指标。完整的 JSON 报告还包含额外指标和设备信息;性能分析生成的 trace 文件及 JSON 报告会从设备复制到主机。原文结果截图用于说明结果出现的位置,并不提供可套用到其他设备的性能基线。

主机文件目录如下,其中各占位项应替换为实际项目、模块和设备信息:

project_root/module/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected/device_id/

设备上的 JSON 通常保存在测试 APK 的外部共享媒体目录,例如 /storage/emulated/0/Android/media/<app_id>/<app_id>-benchmarkData.json。比较结果时,要同时保留设备和构建配置。

处理配置错误

为了避免不准确的测量,库会检查环境是否适合评估接近发布状态的性能:

  • debuggable 应为 false。
  • 应使用物理设备;模拟器不适合作为这里的受支持测量环境。
  • 已 root 的设备应锁定时钟频率。
  • 设备电量至少为 25%。

检查失败时,库会报错。如果确实需要在受限配置下做一次探索性运行,可以把指定错误降为警告,让它们不再中断测试。通过插桩参数 androidx.benchmark.suppressErrors 传入以逗号分隔的错误名称即可。这不会修复导致测量偏差的环境条件。

Kotlin DSL:

android {
    defaultConfig {
       …
      testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "DEBUGGABLE,LOW-BATTERY"
    }
}

Groovy:

android {
    defaultConfig {
       …
      testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "DEBUGGABLE,LOW-BATTERY"
    }
}

这两个片段中的 … 都是省略标记,应保留原有配置,不应原样作为语句写入文件。

命令行也可传递该参数。原文命令写成 $ ./gradlew :benchmark:connectedCheck -P andoidtestInstrumentationRunnerArguments.androidx.benchmark.supperssErrors=DEBUGGABLE,LOW-BATTERY,其中属性前缀与 suppressErrors 均有拼写错误。根据 官方插桩参数说明,修正后是:

./gradlew :benchmark:connectedCheck -P android.testInstrumentationRunnerArguments.androidx.benchmark.suppressErrors=DEBUGGABLE,LOW-BATTERY

上面的 $ 是原文的命令提示符,修正命令已经去掉。压制错误后,库会在测试结果名称前加入相应错误标记。例如可调试构建会添加 DEBUGGABLE_ 前缀,提醒读者该结果是在不理想的配置下取得的。

继续阅读

围绕启动、交互等较大范围的行为,可进一步阅读 Macrobenchmark;基准测试自身的性能分析与不使用 Gradle 的配置也见 Android Developers 的相关文档。

原文代码对照

下列两段保留原文写法,便于对照上文明确说明的修正。第一段 Java 示例结构不完整,第二段命令的参数存在原文拼写问题;两段均未在本机运行,不应直接当作可执行示例。

原文快速开始的 Java 片段

@RunWith(AndroidJUnit4.class)
class SampleBenchmark {
    @Rule
    public BenchmarkRule benchmarkRule = new BenchmarkRule();

    @Test
    public void benchmarkSomeWork() {
            BenchmarkRuleKt.measureRepeated(
                (Function1<BenchmarkRule.Scope, Unit>) scope -> doSomeWork()
            );
       }
    }
}

原文错误抑制参数命令

$ ./gradlew :benchmark:connectedCheck -P andoidtestInstrumentationRunnerArguments.androidx.benchmark.supperssErrors=DEBUGGABLE,LOW-BATTERY

来源与许可

原文:Write a Microbenchmark,Android Developers,页面标示更新日期为 2026-10-01。本文对正文作中文翻译,保留全部示例用途;修正了快速开始 Java 片段与命令行参数拼写,并将四幅界面或关系图的关键教学信息改写为文字及表格。

根据 Android Developers 的 Content License,除另有说明外,文档及其中展示的代码采用 Apache License 2.0。官方性能示例的版权声明为 Copyright 2021 The Android Open Source Project。代码按许可“按原样”提供,不附带明示或默示担保。Java 和 OpenJDK 是 Oracle 及/或其关联公司的商标或注册商标。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容