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 内的 View 层次结构以及 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。例如,注入应用状态信息可以通过为发生卡顿的帧提供上下文来使帧数据更有用。

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

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

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

静态 getHolderForHierarchy() 方法允许您避免必须在某个地方缓存 holder 实例以供以后检索,并使从代码(甚至库代码,否则无法访问原始实例)中的任何位置更轻松地检索现有状态对象。

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

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

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

要控制 UI/应用状态,应用可以使用 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 的应用可能希望为每个 RecyclerViews 提供可识别的键,而不是简单地为每个 RecyclerViews 使用 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 的完整详细信息,请查看我们的 性能示例应用

提供反馈

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

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