JankStats 库

JankStats 库可帮助您跟踪和分析应用中的性能问题。卡顿(Jank)是指渲染时间过长的应用帧,JankStats 库提供有关应用卡顿统计数据的报告。

功能

JankStats 基于现有的 Android 平台功能构建,包括 Android 7(API 级别 24)及更高版本上的 FrameMetrics API 或早期版本上的 OnPreDrawListener。这些机制可帮助应用跟踪帧完成所需的时间。JankStats 库还提供两项附加功能,使其更具动态性且更易于使用:卡顿启发式算法和界面状态。

卡顿启发式算法

虽然您可以使用 FrameMetrics 跟踪帧持续时间,但 FrameMetrics 无法帮助确定实际卡顿。但是,JankStats 具有可配置的内部机制,可以确定何时发生卡顿,从而使报告立即变得更有用。

界面状态

通常需要了解应用中性能问题的上下文。例如,如果您开发了一个使用 FrameMetrics 的复杂多屏应用,并且发现您的应用经常出现严重卡顿的帧,您就需要了解问题发生的位置、用户在做什么以及如何重现该问题,从而将这些信息情境化。

JankStats 通过引入 state API 来解决此问题,该 API 可让您与库通信,以提供有关应用 Activity 的信息。当 JankStats 记录有关卡顿帧的信息时,它会在卡顿报告中包含应用的当前状态。

用法

要开始使用 JankStats,请为每个 Window 实例化并启用该库。每个 JankStats 对象仅在 Window 中跟踪数据。实例化该库需要一个 Window 实例以及一个 OnFrameListener 监听器,两者都用于向客户端发送指标。监听器会在每个帧上调用 FrameData,并详细说明

  • 帧开始时间
  • 持续时间值
  • 该帧是否应被视为卡顿
  • 一组字符串对,其中包含有关帧期间应用状态的信息

为了使 JankStats 更有用,应用应使用相关的界面状态信息填充库,以便在 FrameData 中进行报告。您可以通过 PerformanceMetricsState API(而不是直接通过 JankStats)执行此操作,所有状态管理逻辑和 API 都位于此处。

初始化

要开始使用 JankStats 库,请首先将 JankStats 依赖项添加到您的 Gradle 文件中

implementation "androidx.metrics:metrics-performance:1.0.0-beta02"

接下来,为每个 Window 初始化并启用 JankStats。当 Activity 进入后台时,您还应暂停 JankStats 跟踪。在您的 Activity 替换中创建并启用 JankStats 对象

class JankLoggingActivity : AppCompatActivity() {

    private lateinit var jankStats: JankStats


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // metrics state holder can be retrieved regardless of JankStats initialization
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // initialize JankStats for current window
        jankStats = JankStats.createAndTrack(window, jankFrameListener)

        // add activity name as state
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
        // ...
    }

上述示例在构建 JankStats 对象后注入有关当前 Activity 的状态信息。为此 JankStats 对象创建的所有未来 FrameData 报告现在也包含 Activity 信息。

JankStats.createAndTrack 方法接收一个 Window 对象的引用,该对象是该 Window 内部视图层次结构以及 Window 本身的代理。jankFrameListener 会在用于将信息从平台内部传递到 JankStats 的同一线程上调用。

要在任何 JankStats 对象上启用跟踪和报告,请调用 isTrackingEnabled = true。尽管默认情况下已启用,但暂停 Activity 会禁用跟踪。在这种情况下,请务必在继续之前重新启用跟踪。要停止跟踪,请调用 isTrackingEnabled = false

override fun onResume() {
    super.onResume()
    jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    jankStats.isTrackingEnabled = false
}

报告

JankStats 库将所有数据跟踪(针对每个帧)报告给已启用 JankStats 对象的 OnFrameListener。应用可以存储和汇总此数据,以便稍后上传。如需了解详情,请查看“聚合”部分中提供的示例。

您需要创建并提供 OnFrameListener,以便您的应用接收每帧报告。此监听器会在每个帧上调用,以向应用提供持续的卡顿数据。

private val jankFrameListener = JankStats.OnFrameListener { frameData ->
    // A real app could do something more interesting, like writing the info to local storage and later on report it.
    Log.v("JankStatsSample", frameData.toString())
}

监听器会通过 FrameData 对象提供有关卡顿的每帧信息。这包含有关请求帧的以下信息

如果您使用的是 Android 12(API 级别 31)或更高版本,可以使用以下方法公开有关帧持续时间的更多数据

在监听器中使用 StateInfo 来存储有关应用状态的信息。

请注意,OnFrameListener 会在内部用于将每帧信息传递到 JankStats 的同一线程上调用。在 Android 6(API 级别 23)及更低版本上,它是主 (UI) 线程。在 Android 7(API 级别 24)及更高版本上,它是为 FrameMetrics 创建和使用的线程。无论哪种情况,处理回调并快速返回以防止该线程出现性能问题都非常重要。

另外请注意,回调中发送的 FrameData 对象在每个帧上都会被重用,以避免为数据报告分配新对象。这意味着您必须将该数据复制并缓存到其他位置,因为该对象在回调返回后应被视为无状态和过时。

聚合

您可能希望您的应用代码聚合每帧数据,这允许您自行保存和上传信息。尽管保存和上传的详细信息超出了 alpha 版 JankStats API 发布的范围,但您可以使用我们 GitHub 仓库中提供的 JankAggregatorActivity 来查看一个初步的 Activity,用于将每帧数据聚合到更大的集合中。

JankAggregatorActivity 使用 JankStatsAggregator 类在 JankStats OnFrameListener 机制之上分层其自身的报告机制,从而为仅报告跨越多个帧的信息集合提供更高级别的抽象。

不直接创建 JankStats 对象,而是 JankAggregatorActivity 创建一个 JankStatsAggregator 对象,该对象在内部创建其自己的 JankStats 对象

class JankAggregatorActivity : AppCompatActivity() {

    private lateinit var jankStatsAggregator: JankStatsAggregator


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // Metrics state holder can be retrieved regardless of JankStats initialization.
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // Initialize JankStats with an aggregator for the current window.
        jankStatsAggregator = JankStatsAggregator(window, jankReportListener)

        // Add the Activity name as state.
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
    }

JankAggregatorActivity 中使用了一个类似的机制来暂停和恢复跟踪,并添加了 pause() 事件作为信号,通过调用 issueJankReport() 来发出报告,因为生命周期更改似乎是捕获应用中卡顿状态的合适时机

override fun onResume() {
    super.onResume()
    jankStatsAggregator.jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    // Before disabling tracking, issue the report with (optionally) specified reason.
    jankStatsAggregator.issueJankReport("Activity paused")
    jankStatsAggregator.jankStats.isTrackingEnabled = false
}

上述示例代码是应用启用 JankStats 并接收帧数据所需的全部内容。

管理状态

您可能需要调用其他 API 来自定义 JankStats。例如,注入应用状态信息可以通过为发生卡顿的帧提供上下文,使帧数据更有用。

此静态方法会检索给定视图层次结构的当前 MetricsStateHolder 对象。

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

可使用活动层次结构中的任何视图。在内部,这会检查是否存在与该视图层次结构关联的现有 Holder 对象。此信息缓存到该层次结构顶部的视图中。如果不存在此类对象,getHolderForHierarchy() 会创建一个。

静态 getHolderForHierarchy() 方法可让您避免将 Holder 实例缓存到某个位置以便以后检索,并且使从代码中任何位置(甚至库代码,否则无法访问原始实例)检索现有状态对象变得更容易。

请注意,返回值是一个持有者对象,而不是状态对象本身。持有者内部状态对象的值仅由 JankStats 设置。也就是说,如果应用为包含该视图层次结构的窗口创建 JankStats 对象,则会创建并设置状态对象。否则,如果没有 JankStats 跟踪信息,则不需要状态对象,并且应用或库代码也无需注入状态。

这种方法使得检索 JankStats 可以填充的持有者成为可能。外部代码可以随时请求持有者。调用者可以缓存轻量级 Holder 对象,并随时使用它来设置状态,具体取决于其内部 state 属性的值,如下面的示例代码所示,其中仅当持有者的内部 state 属性非 null 时才设置状态

val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)
// ...
metricsStateHolder.state?.putState("Activity", javaClass.simpleName)

要控制界面/应用状态,应用可以使用 putStateremoveState 方法注入(或移除)状态。JankStats 会记录这些调用的时间戳。如果帧与状态的开始和结束时间重叠,JankStats 会报告该状态信息以及帧的计时数据。

对于任何状态,添加两条信息:key(状态类别,例如“RecyclerView”)和 value(当时发生情况的信息,例如“滚动”)。

当状态不再有效时,使用 removeState() 方法移除状态,以确保不会将错误或误导性信息与帧数据一起报告。

使用之前添加的 key 调用 putState() 会用新值替换该状态的现有 value

状态 API 的 putSingleFrameState() 版本会添加一个状态,该状态仅在下一个报告帧上记录一次。系统在此之后会自动将其移除,确保您的代码中不会意外出现过时状态。请注意,removeState() 没有 singleFrame 等效项,因为 JankStats 会自动移除单帧状态。

private val scrollListener = object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        // check if JankStats is initialized and skip adding state if not
        val metricsState = metricsStateHolder?.state ?: return

        when (newState) {
            RecyclerView.SCROLL_STATE_DRAGGING -> {
                metricsState.putState("RecyclerView", "Dragging")
            }
            RecyclerView.SCROLL_STATE_SETTLING -> {
                metricsState.putState("RecyclerView", "Settling")
            }
            else -> {
                metricsState.removeState("RecyclerView")
            }
        }
    }
}

请注意,用于状态的键应该足够有意义,以便进行后续分析。特别地,由于具有与之前添加的相同 key 的状态将替换早期值,您应该尝试为在您的应用或库中可能具有不同实例的对象使用唯一的 key 名称。例如,一个具有五个不同 RecyclerViews 的应用可能希望为每个 RecyclerView 提供可识别的键,而不是简单地为每个 RecyclerView 使用 RecyclerView,然后在结果数据中无法轻易分辨帧数据指的是哪个实例。

卡顿启发式算法

要调整用于确定什么是卡顿的内部算法,请使用 jankHeuristicMultiplier 属性。

默认情况下,系统将卡顿定义为渲染时间是当前刷新率两倍的帧。它不会将任何超过刷新率的情况视为卡顿,因为关于应用渲染时间的信息并不完全清晰。因此,最好添加一个缓冲区,并且仅在问题导致明显的性能问题时才报告。

这两个值都可以通过这些方法进行更改,以更密切地适应应用的情况,或者在测试中根据需要强制卡顿发生或不发生。

在 Jetpack Compose 中使用

目前,在 Compose 中使用 JankStats 所需的设置非常少。为了在配置更改时保留 PerformanceMetricsState,请按如下方式记住它

/**
 * Retrieve MetricsStateHolder from compose and remember until the current view changes.
 */
@Composable
fun rememberMetricsStateHolder(): PerformanceMetricsState.Holder {
    val view = LocalView.current
    return remember(view) { PerformanceMetricsState.getHolderForHierarchy(view) }
}

要使用 JankStats,请将当前状态添加到 stateHolder 中,如下所示

val metricsStateHolder = rememberMetricsStateHolder()

// Reporting scrolling state from compose should be done from side effect to prevent recomposition.
LaunchedEffect(metricsStateHolder, listState) {
    snapshotFlow { listState.isScrollInProgress }.collect { isScrolling ->
        if (isScrolling) {
            metricsStateHolder.state?.putState("LazyList", "Scrolling")
        } else {
            metricsStateHolder.state?.removeState("LazyList")
        }
    }
}

有关在 Jetpack Compose 应用中使用 JankStats 的完整详细信息,请查看我们的性能示例应用

提供反馈

通过以下资源与我们分享您的反馈和想法

问题跟踪器
报告问题,以便我们修复 bug。