JankStats 库

JankStats 库可帮助您跟踪和分析应用中的性能问题。Jank 指的是渲染时间过长的应用帧,JankStats 库提供应用 Jank 统计信息的报告。

功能

JankStats 基于现有的 Android 平台功能构建,包括 Android 7(API 级别 24)及更高版本上的 FrameMetrics API 或早期版本上的 OnPreDrawListener。这些机制可以帮助应用跟踪帧完成所需的时间。JanksStats 库提供了另外两种功能,使它更具动态性和易用性:Jank 启发式方法和 UI 状态。

Jank 启发式方法

您可以使用 FrameMetrics 来跟踪帧持续时间,但 FrameMetrics 无法帮助确定实际的 Jank。但是,JankStats 具有可配置的内部机制来确定何时发生 Jank,从而使报告更直接有用。

UI 状态

通常需要了解应用中性能问题的上下文。例如,如果您开发了一个复杂的、多屏幕的应用并使用了 FrameMetrics,并且发现您的应用经常出现 Jank 帧,您需要通过了解问题发生的位置、用户正在执行的操作以及如何复制问题来将这些信息放在上下文中。

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

用法

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

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

为了使 JankStats 更实用,应用程序应使用相关的 UI 状态信息填充库,以便在 FrameData 中进行报告。您可以通过PerformanceMetricsState API(而不是直接使用 JankStats)来实现此目的,其中包含所有状态管理逻辑和 API。

初始化

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

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

接下来,为每个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对象提供有关卡顿的每帧信息。这包含以下有关请求帧的信息

  • isJank:一个布尔标志,指示帧中是否发生了卡顿。
  • frameDurationUiNanos:帧的持续时间(以纳秒为单位)。
  • frameStartNanos:帧开始的时间(以纳秒为单位)。
  • states:帧期间应用的状态。

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

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

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

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

聚合

您可能希望您的应用代码聚合每帧数据,这使您可以自行决定保存和上传信息。尽管保存和上传的细节超出了 alpha JankStats API 版本的范围,但您可以查看使用JankAggregatorActivity将每帧数据聚合到更大的集合中的初步 Activity,该 Activity 可在我们的GitHub 存储库中找到。

JankAggregatorActivity 使用JankStatsAggregator类在其自身的报告机制之上添加一层,该机制位于 JankStats OnFrameListener机制之上,以提供更高级别的抽象,仅报告跨越多个帧的信息集合。

JankAggregatorActivity不会直接创建 JankStats 对象,而是创建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 实例以供以后检索,并使从代码中的任何位置(甚至是库代码,否则无法访问原始实例)更容易检索现有状态对象。

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

这种方法使得可以检索 JankStats 随后可以填充的 holder。外部代码可以随时请求 holder。调用者可以缓存轻量级Holder对象,并根据其内部state属性的值随时使用它来设置状态,如下面的示例代码所示,其中仅在 holder 的内部 state 属性非空时才设置 state

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

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

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

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

使用先前添加的key调用putState()会用新的value替换该状态的现有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的状态将替换该较早的值,因此您应该尝试为应用或库中可能具有不同实例的对象使用唯一的key名称。例如,具有五个不同 RecyclerView 的应用可能希望为每个 RecyclerView 提供可识别的 key,而不是简单地为每个 RecyclerView 使用RecyclerView,然后无法轻松地判断生成的數據指的是哪个实例。

Jank 启发式方法

要调整用于确定什么被视为卡顿的内部算法,请使用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。