Android JankStats实现解析

JankStats 是安卓 JetPack里新出的一个专门用来检测帧卡顿的库。并且支持各个安卓版本。我们来分析一下他的实现。

JankStats使用比较简单,就下面一些代码配置:

plain 复制代码
private val jankFrameListener = OnFrameListener { frameData->
  Log.e("lei",frameData.isJank.toString()) // isJank为true表示卡顿
} 


val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(window.decorView)
jankStats = JankStats.createAndTrack(window, jankFrameListener) // 绑定listener
metricsStateHolder.state?.putState("Activity", javaClass.simpleName)

关键点还是在于不同安卓版本里,JankStats是如何定义卡顿的

createAndTrack 方法里面创建了 JankStats 对象。对象里面持有了真正的实现类,并且根据安卓版本做了区分:

他们的继承关系如下:

其中api16和api24为两种主要实现,api26和api31是基于api24做了很小的改动。通过setupFrameTimer(true)来执行真正的初始化。

api16

api16给decorView绑定了PreDrawListener来做渲染监听,这个Listener对象会通过tag的方式根据decorView保留来复用:

plain 复制代码
override fun onPreDraw(): Boolean {
  val decorView = decorViewRef.get()
  decorView?.let {
    val frameStart = getFrameStartTime()
    with(decorView) {
      handler.sendMessageAtFrontOfQueue(
        Message.obtain(handler) {
          val now = System.nanoTime()
          val expectedDuration = getExpectedFrameDuration(decorView)
          synchronized(this@DelegatingOnPreDrawListener) {
            for (delegate in delegates) {
              delegate.onFrame(frameStart, now - frameStart, expectedDuration)
            }
          }
          metricsStateHolder.state?.cleanupSingleFrameStates()
        }
        .apply { MessageCompat.setAsynchronous(this, true) }
      )
    }
  }
}

核心就是:

  • 获取帧开始的时间:getFrameStartTime里面反射获取了 Choreographer 的 mLastFrameTimeNanos 字段。接着往主线程队列的最前面发送一个消息,当这个消息被处理了,就认为这一帧结束
  • 计算帧预期的持续时间:getExpectedFrameDuration里获取了当前屏幕的刷新率,屏幕刷新率表示的是1s内渲染的帧数,所以这里通过 1s / 刷新率来获取:
plain 复制代码
JankStatsBaseImpl.frameDuration =
    (1000 / refreshRate * JankStatsBaseImpl.NANOS_PER_MS).toLong()
return JankStatsBaseImpl.frameDuration
  • 回调onFrame,这里此帧的ui耗时计算为现在时间减去帧开始时间

  • 像上层回掉JankStats的onFrame,计算是否掉帧。

    当ui耗时大于预期耗时的系数(默认2倍)的时候,认为发生了jank:

plain 复制代码
internal open fun getFrameData(
    startTime: Long,
    uiDuration: Long,
    expectedDuration: Long
): FrameData {
    metricsStateHolder.state?.getIntervalStates(startTime, startTime + uiDuration, stateInfo)
    val isJank = uiDuration > expectedDuration
    frameData.update(startTime, uiDuration, isJank)
    return frameData
}

这里一张图来表示api16的检测原理:

api24

接着来看api24的实现,从api24开始,安卓系统提供了一个更为准确的获取帧回掉和帧状态的api,叫做FrameMetrics,api24及以上的JankStats实现便是基于这个api实现。

Window通过 addOnFrameMetricsAvailableListener 添加一个 OnFrameMetricsAvailableListener, listener在每一帧结束,并且帧相关的各个指标数据都准备好的时候,会回调 onFrameMetricsAvailable 方法,每次回调的时候会带上 FrameMetrics

FrameMetrics会携带很多数据指标,基本对应了安卓渲染每一帧的每个阶段:

字段 含义
UNKNOWN_DELAY_DURATION ui线程响应耗时
INPUT_HANDLING_DURATION 事件输入处理耗时
ANIMATION_DURATION 动画耗时
LAYOUT_MEASURE_DURATION 测量、布局的耗时
DRAW_DURATION 绘制耗时
SYNC_DURATION display lists同步渲染线程的耗时
COMMAND_ISSUE_DURATION 向gpu发出渲染命令耗时
SWAP_BUFFERS_DURATION 把帧的缓冲区发送给显示子系统耗时
TOTAL_DURATION 一帧的实际总耗时
INTENDED_VSYNC_TIMESTAMP 帧的预期起点时间戳
VSYNC_TIMESTAMP 帧vsync信号时间戳
GPU_DURATION 这一帧在gpu上花费的时间
DEADLINE 系统分配给这一帧的时间

JankStats在24上的实现就基于这些:

plain 复制代码
delegator = DelegatingFrameMetricsListener(delegates)
if (frameMetricsHandler == null) {
  val thread = HandlerThread("FrameMetricsAggregator")
  thread.start()
  frameMetricsHandler = Handler(thread.looper)
}
window.addOnFrameMetricsAvailableListener(delegator, frameMetricsHandler)

这里注册了一个 DelegatingFrameMetricsListener,传入了一个HandlerThread的handler,这是为了避免在主线程收到帧回掉的时候使用方加入耗时的逻辑。

DelegatingFrameMetricsListener 内部就是一一回掉我们添加的 OnFrameMetricsAvailableListener 的 onFrameMetricsAvailable。

api24添加的 OnFrameMetricsAvailableListener#onFrameMetricsAvailable 如下:

plain 复制代码
val startTime = max(getFrameStartTime(frameMetrics), prevEnd)
if (startTime >= listenerAddedTime && startTime != prevStart) {
  val expectedDuration = 
    getExpectedFrameDuration(frameMetrics) * jankStats.jankHeuristicMultiplier
  jankStats.logFrameData(
    getFrameData(startTime, expectedDuration.toLong(), frameMetrics)
  )
  prevStart = startTime
}

关键步骤:

  • getFrameStartTime: 计算开始时间,和计算的上一帧结束时间取更靠后的一个
  • getExpectedFrameDuration:计算预期的非卡顿帧耗时并乘上系数
  • getFrameData:生成FrameData对象,计算是否发生jank

检测原理同样用图表示一波:

上述几个方法的实现会根据FrameMetrics api的变化有所调整

  1. 帧开始时间
    1. api24: 和api16一样,反射获取Choreographer#mLastFrameTimeNanos
    2. api>=26: 读取FrameMetrics的INTENDED_VSYNC_TIMESTAMP
  2. 预期帧耗时
    1. api24: 和api16一样,通过刷新率去计算
    2. api>=31: 读取FraeMetrics的DEADLINE
  3. 判断是否jank
    1. api24: ui耗时为FrameMetrics ui线程响应到同步给渲染线程的耗时,也就是 UNKNOWN_DELAY_DURATION+INPUT_HANDLING_DURATION+ANIMATION_DURATION+LAYOUT_MEASURE_DURATION+DRAW_DURATION+SYNC_DURATION,cpu耗时为TOTAL_DURATION,如果ui耗时大于预期的帧耗时(乘以系数),就认为发生了卡顿。
    2. api>=31: ui耗时算法和jank对比方式同24,cpu耗时改为TOTAL_DURATION减去gpu耗时(GPU_DURATION)再加上SWAP_BUFFERS_DURATION。计算相比api24更为精密。

总结

这里就过了一遍JankStats的使用和实现原理。了解内部细节尤其是不同版本的实现可以帮助我们更好的理解他的工作机制。我们来思考基于下这个库能做什么:

  1. 帧率卡顿监控,这个框架直接给出了结果,渲染情况比较明了
  2. 优化主线程卡顿方案的卡顿监控时机,例如把出现冻帧的时候作为主线程过于耗时的判断标准,更准确
  3. 利用FrameMetrics的详细数据做出更复杂的帧率监控,帮助定位大约什么原因、什么阶段导致了掉帧
相关推荐
CL_IN3 小时前
高效集成销售订单数据到MySQL的方法
android·数据库·mysql
Vesper634 小时前
【Android】‘adb shell input text‘ 模拟器输入文本工具使用教程
android·adb
MyhEhud5 小时前
Kotlin apply 方法的用法和使用场景
android·kotlin·kotlin apply函数
Code额5 小时前
MySQL的事务机制
android·mysql·adb
蓝莓浆糊饼干7 小时前
请简述一下String、StringBuffer和“equals”与“==”、“hashCode”的区别和使用场景
android·java
李斯维8 小时前
深入理解 Android Canvas 变换:缩放、旋转、平移全解析(一)
android·canvas·图形学
_一条咸鱼_8 小时前
Android Retrofit 框架日志与错误处理模块深度剖析(七)
android
顾林海8 小时前
Flutter Dart 面向对象编程全面解析
android·前端·flutter
去伪存真9 小时前
摸着石头过河,重新支棱起Capacitor老项目
android·前端