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的变化有所调整
- 帧开始时间
- api24: 和api16一样,反射获取Choreographer#mLastFrameTimeNanos
- api>=26: 读取FrameMetrics的INTENDED_VSYNC_TIMESTAMP
- 预期帧耗时
- api24: 和api16一样,通过刷新率去计算
- api>=31: 读取FraeMetrics的DEADLINE
- 判断是否jank
- api24: ui耗时为FrameMetrics ui线程响应到同步给渲染线程的耗时,也就是 UNKNOWN_DELAY_DURATION+INPUT_HANDLING_DURATION+ANIMATION_DURATION+LAYOUT_MEASURE_DURATION+DRAW_DURATION+SYNC_DURATION,cpu耗时为TOTAL_DURATION,如果ui耗时大于预期的帧耗时(乘以系数),就认为发生了卡顿。
- api>=31: ui耗时算法和jank对比方式同24,cpu耗时改为TOTAL_DURATION减去gpu耗时(GPU_DURATION)再加上SWAP_BUFFERS_DURATION。计算相比api24更为精密。
总结
这里就过了一遍JankStats的使用和实现原理。了解内部细节尤其是不同版本的实现可以帮助我们更好的理解他的工作机制。我们来思考基于下这个库能做什么:
- 帧率卡顿监控,这个框架直接给出了结果,渲染情况比较明了
- 优化主线程卡顿方案的卡顿监控时机,例如把出现冻帧的时候作为主线程过于耗时的判断标准,更准确
- 利用FrameMetrics的详细数据做出更复杂的帧率监控,帮助定位大约什么原因、什么阶段导致了掉帧