JankStats 库

JankStats 库可帮助您跟踪和分析应用的性能问题。卡顿是指呈现应用帧用时过长的情况,JankStats 库提供关于应用卡顿统计信息的报告。

功能

JankStats 基于现有的 Android 平台功能构建,包括 Android 7(API 级别 24)及更高版本中的 FrameMetrics API 或更低版本中的 OnPreDrawListener。这些机制可以帮助应用跟踪完成帧所用的时间。JanksStats 库提供了两项额外的功能,使其更加动态且更易于使用,即:卡顿启发法和界面状态。

卡顿启发法

虽然您可以使用 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-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)及更低版本中,该线程是主(界面)线程。在 Android 版本 7(API 级别 24)及更高版本中,该线程是为 FrameMetrics 创建并由其使用的线程。无论是哪种情况,都必须快速处理回调并快速返回,以防该线程出现性能问题。

另请注意,回调中发送的 FrameData 对象会在每一帧中重复使用,以便避免必须分配新对象用于数据报告。这意味着您必须复制数据并将其缓存在其他位置,因为一旦回调返回,该对象就会被视作过时和作废。

聚合

您可能希望应用代码聚合每帧数据,从而让您可以自行决定保存和上传信息。虽然 Alpha 版 JankStats API 无法提供关于保存和上传操作的详细信息,但您可以利用 GitHub 代码库中提供的 JankAggregatorActivity 查看将每帧数据聚合到大型集合的初步 activity。

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() 方法,您无需将容器实例缓存到某处以备日后检索,因而可以更轻松地从代码(甚至库代码;库代码不会以其他方式访问原始实例)中的任何位置检索现有的状态对象。

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

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

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

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

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

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

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

putSingleFrameState() 版状态 API 添加了一个状态,该状态仅对下一个报告的帧记录一次。之后,系统会自动将其移除,以确保您不会意外地在代码中看到已过时的状态。请注意,没有等同于 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 名称。例如,有五个不同 RecyclerView 的应用可能需要针对每个 RecyclerView 提供可识别的键,而不是每一个 RecyclerView 都简单地使用 RecyclerView,然后无法在得到的数据中轻松辨别出帧数据指的是哪个实例。

Jank heuristics(卡顿启发法)

如需调整用于确定何为卡顿的内部算法,请使用 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,请参阅我们的性能示例应用

提供反馈

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

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