捕获 Macrobenchmark 指标

指标是指从基准测试中提取的主要信息类型。这些指标会以 List 的形式传递给 measureRepeated 函数;也就是说,您可以一次性指定多个测量指标。若要运行基准测试,必须至少提供 1 种类型的指标。

以下代码段会捕获帧时间和自定义的轨迹部分指标:

Kotlin

benchmarkRule.measureRepeated(
    packageName = TARGET_PACKAGE,
    metrics = listOf(
        FrameTimingMetric(),
        TraceSectionMetric("RV CreateView"),
        TraceSectionMetric("RV OnBindView"),
    ),
    iterations = 5,
    // ...
)

Java

benchmarkRule.measureRepeated(
    TARGET_PACKAGE,     // packageName
    Arrays.asList(      // metrics
        new StartupTimingMetric(),
        new TraceSectionMetric("RV CreateView"),
        new TraceSectionMetric("RV OnBindView"),
    ),
    5,                  // Iterations
    // ...
);

在这个示例中,RV CreateViewRV OnBindViewRecyclerView 中定义的可跟踪代码块的 ID。createViewHolder() 方法的源代码示例展示了如何在您自己的代码中定义可跟踪代码块。

本文稍后会详细介绍 StartupTimingMetricTraceSectionMetricFrameTimingMetricPowerMetric

基准测试结果会输出到 Android Studio,如图 1 所示。如果定义了多个指标,则所有这些指标会在输出中合并到一处。

TraceSectionMetric 和 FrameTimingMetric 的结果。
图 1. TraceSectionMetricFrameTimingMetric 的结果。

StartupTimingMetric

StartupTimingMetric 可捕获提供以下值的应用启动时间指标:

  • timeToInitialDisplayMs:从系统收到启动 intent 到渲染目标 Activity 的第一帧所用的时间。
  • timeToFullDisplayMs:从系统收到启动 intent 到应用使用 reportFullyDrawn() 方法报告已完全绘制所用的时间。当跟随在 reportFullyDrawn() 调用之后或包含该调用的第一帧完成渲染时,测量即会停止。Android 10(API 级别 29)及更低版本可能不支持此测量功能。

StartupTimingMetric 会输出启动迭代的最小值、中位数值和最大值。如需评估启动方面的改进,您应重点关注中位数值,因为它们能够最准确地估算典型启动时间。如需详细了解影响应用启动时间的各个因素,请参阅应用启动时间

StartupTimingMetric 结果
图 2. StartupTimingMetric 结果。

提高启动时间准确性

衡量应用启动时间的两个关键指标是初步显示所用时间 (TTID)完全显示所用时间 (TTFD)。 TTID 是显示应用界面的第一帧所需的时间。TTFD 还包含当显示第一帧后显示异步加载的所有内容所需的时间。

系统会在调用 ComponentActivityreportFullyDrawn() 方法后报告 TTFD。如果从未调用 reportFullyDrawn(),则改为报告 TTID。如果系统调用 reportFullyDrawn(),可能需要延迟到异步加载完成之后再报告相应时间。例如,如果界面包含一个动态列表(如 RecyclerView)或延迟列表,那么可能就要在首次绘制列表之后、因此也就是在界面被标记为完全绘制之后,才通过后台任务来填充该列表。在这种情况下,基准测试不涵盖列表填充。

如需将列表填充时间也纳入基准时间,请使用 getFullyDrawnReporter() 获取 FullyDrawnReporter,并在应用代码中为其添加报告程序。当后台任务完成列表填充后,您必须释放报告程序。

在所有添加的报告程序都被释放之后,FullyDrawnReporter 才会调用 reportFullyDrawn() 方法。通过添加报告程序并在后台进程完成后将其释放,还可在启动时间数据中包含填充列表所用的时间。这不会改变面向用户的应用行为,但可让启动时间数据包含填充列表所需的时间。

以下示例展示了如何同时运行多个后台任务,并让每个任务都注册自己的报告程序:

Kotlin

class MainActivity : ComponentActivity() {

    sealed interface ActivityState {
        data object LOADING : ActivityState
        data object LOADED : ActivityState
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var activityState by remember {
                mutableStateOf(ActivityState.LOADING as ActivityState)
            }
            fullyDrawnReporter.addOnReportDrawnListener {
                activityState = ActivityState.LOADED
            }
            ReportFullyDrawnTheme {
                when(activityState) {
                    is ActivityState.LOADING -> {
                        // Display the loading UI.
                    }
                    is ActivityState.LOADED -> {
                        // Display the full UI.
                    }
                }
            }
            SideEffect {
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
            }
        }
    }
}

Java

public class MainActivity extends ComponentActivity {
    private FullyDrawnReporter fullyDrawnReporter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        fullyDrawnReporter = getFullyDrawnReporter();
        fullyDrawnReporter.addOnReportDrawnListener(() -> {
            // Trigger the UI update.
            return Unit.INSTANCE;
        });

        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

               fullyDrawnReporter.removeReporter();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

                fullyDrawnReporter.removeReporter();
            }
        }).start();
    }
}

无论所有其他任务的执行顺序如何,都应始终将 reportFullyDrawn() 放在最后一步执行。

如果您的应用使用 Jetpack Compose,您可以使用以下 API 指示完全绘制状态:

  • ReportDrawn:表示可组合项刚刚准备好进行互动。
  • ReportDrawnWhen:接受一个谓词(例如 list.count > 0),用于指明可组合项何时准备好进行互动。
  • ReportDrawnAfter:接受一个暂停方法,该方法一旦完成即表示可组合项已准备好进行互动。

FrameTimingMetric

FrameTimingMetric 会捕获基准测试所生成帧(如滚动或动画)的时间信息,并输出以下值:

  • frameOverrunMs:给定帧完全呈现用时与其呈现时限相差的时长。正数表示出现了丢帧和可见的卡顿。负数表示帧完全呈现用时比呈现时限少多少时间。注意:此指标仅适用于 Android 12(API 级别 31)及更高版本。
  • frameDurationCpuMs:通过界面线程和 RenderThread 在 CPU 中生成帧所用的时间。

系统会收集分布在第 50 个、第 90 个、第 95 个和第 99 个百分位的以上测量值。

如需详细了解如何发现和改进呈现速度缓慢的帧,请参阅呈现速度缓慢

FrameTimingMetric 结果
图 3. FrameTimingMetric 结果。

TraceSectionMetric

TraceSectionMetric 会捕获与提供的 sectionName 匹配的轨迹部分发生的次数和所花费的时间。它会输出该时间的最小值、中位数和最大值(以毫秒为单位)。轨迹部分通过 trace(sectionName) 函数调用或者 Trace.beginSection(sectionName)Trace.endSection() 之间的代码(或其异步变体之间的代码)来定义。该指标始终选择测量期间所捕获的第一个轨迹部分实例。默认情况下,它仅输出您的软件包中的轨迹部分;如需加入软件包以外的进程,请设置 targetPackageOnly = false

如需详细了解如何进行跟踪,请参阅系统跟踪概览定义自定义事件

TraceSectionMetric
图 4. TraceSectionMetric 结果。

PowerMetric

PowerMetric 会捕获所提供的电源类别在测试期间的耗电量或能量变化。 每个选定类别可拆分为多个可测量的子组件,未选择的类别则添加到“未选择”指标中。

以下指标测量的是系统级消耗,而不是每个应用的消耗,并且只适用于 Pixel 6、Pixel 6 Pro 及更新型号的设备:

  • power<category>Uw - 此类别在测试期间的耗电量。
  • energy<category>Uws - 此类别在测试期间每单位时间传输的能量。

类别包括以下各项:

  • CPU
  • DISPLAY
  • GPU
  • GPS
  • MEMORY
  • MACHINE_LEARNING
  • NETWORK
  • UNCATEGORIZED

对于某些类别(如 CPU),可能很难将其他进程执行的工作与您自己的应用执行的工作区分开来。请移除或限制不必要的应用和账号,最大限度地减少干扰。

PowerMetric 结果
图 5. PowerMetric 结果。