Android 流畅性监控的三板斧,这里所指是【帧率的监控】,【卡顿监控】和【ANR 的监控】。之所以讲这三者放在一起是他们的联系比较密切。帧率的下降往往伴随着有卡顿,【过分卡顿】往往就会产生 ANR。
严谨的讲,帧率下降不一定会有卡顿(这里对卡顿是从技术角度定义在主线程执行了耗时任务),卡顿产生的原因还有其他因素导致,比如系统负载、CPU 繁忙等。关于卡顿的详细内容放在流畅性三板斧的第二篇。
【过分的卡顿】也不一定产生 ANR,卡顿但未触发 ANR 产生的条件就不会产生 ANR。关于 ANR 的详细内容我们放在三板斧系列文章的第三篇。
该篇我们从应用开发者的角度,探索在应用层监控帧率的四种方式。
温馨提示,本文涉及的实现的代码以上传至 github https://github.com/drummor/GetHandsDirty,结合代码食用更佳
帧率(Frame rate)是以帧称为单位的位图图像连续出现在显示器上的频率(速率)。
线下开发我们可以使用开发者选项的帧率监控或者 adb shell dumpsys gfxinfo packagename
进行监控针对性优化。这些方案不能带到线上。
惯常我们在 Android 里线下对帧率的监控主要依托 Choreographer,关于 Choreographer 不再赘述在其他的文章有比较全面的介绍可以看这两篇文章
利用 Choreographer 的 postcallback 方法接口轮询方式,能够对帧率进行统计。
choreographer.postCallback()
内部是挂载了一个CALLBACK_ANIMATION
类型的 callback。轮训方式往choreographer
内添加 callback,相邻两个 callback 执行时间间隔即能粗略统计单帧的耗时。严谨的讲这不是单帧的耗时而是两个【半帧】拼凑的耗时。
代码示例如下。
class PoorFrameTracker {
private var mLastFrameTime = -1L
private var mFrameCount: Int = 0
val calRate = 200 //ms
fun startTrack() {
mLastFrameTime = 0L
mFrameCount = 0
Choreographer.getInstance().postFrameCallback(object : FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (mLastFrameTime == -1L) {
mLastFrameTime = frameTimeNanos
}
val diff = (frameTimeNanos - mLastFrameTime) / 1_000_000.0f
if (diff > calRate) {
var fps = mFrameCount / diff * 1000
if (fps > 60) {fps = 60.0f}
//todo :统计
mFrameCount = 0
mLastFrameTime = -1
} else {
mFrameCount++
}
Choreographer.getInstance().postFrameCallback(this);
}
})
}
}
针对章节三的方案,首先我们有两个主要的优化方向希望在主线程不活动的时候不进行帧率的检测
我们调用公开 api Choreographer.postCallback()
时会触发垂直同步(这部分可以参考另一篇文章)。
# choreographer
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private long mTimestampNanos;
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
...
mTimestampNanos = timestampNanos;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
...
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}
}
Choreographer
中的一段代码。当收到底层垂直同步信号的时,利用 Handler 机制 post 的一个 Runable,执行该帧的动作doFrame()
。依次我们可以采集到每帧的开始和结束。# Choreographer
private final CallbackQueue[] mCallbackQueues;
mTimestampNanos
void doFrame(long frameTimeNanos, int frame, DisplayEventReceiver.VsyncEventData vsyncEventData) {
...
final long frameIntervalNanos = vsyncEventData.frameInterval;
doCallbacks(Choreographer.CALLBACK_INPUT, frameData, frameIntervalNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameData, frameIntervalNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameData, frameIntervalNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameData, frameIntervalNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameData, frameIntervalNanos);
...
}
补充
doFrame
部分。其他线程和进程还会执行其他动作最终才能完成一帧的绘制。但对于我们应用层来说更关注监控doFrame
,我们在应用开发层面大部分能够干预的也在doFrame
这部分。(方案思路 Matrix)
关于这个方案可查看: https://github.com/drummor/GetHandsDirty
#View
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
...
final AttachInfo ai = mAttachInfo;
if (ai != null) {
ai.mViewScrollChanged = true;
}
...
}
onScrollChanged()
,当该方法调用的时候,会将 mAttachInfo 的 mViewScrollChanged 值设为 true#ViewRootImpl
private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
...
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}
}
mAttachInfo.mViewScrollChanged
值为 true 就会就会调用ViewTreeObserver
的dispatchOnScrollChanged()
方法,只要我们在viewTreeObserver
设置监听,就能获取到界面是否正在滑动这一重要事件。整个过程的如上图所示,我们收到滑动回调这一事件的时候,其实是 choreographer 的 doFrame()调用而来。
结合上面我们就可以在收到【滑动事件】的时候使用 Choreographer 的 postCallback 开始统计帧率。
什么时候结束呢?在没有【滑动信息】生成出来的时候看下面代码
private var isScroll = false
init {
window.decorView.viewTreeObserver.addOnScrollChangedListener {
//标识正在滑动
isScroll = true
//开始统计帧率
Choreographer.getInstance().postFrameCallback(FrameCallback())
}
}
private inner class FrameCallback : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (isScroll) {
isScroll = false //重置滑动状态
if (lastFrameTime != 0L) {
val dropFrame =
(((frameTimeNanos - lastFrameTime) / 1000000f / 16.6667f) + 1f).toInt()
notifyListener(dropFrame)
}
lastFrameTime = frameTimeNanos
} else {
lastFrameTime = 0
}
}
}
这样我们就实现了一个监控滑动帧率的方案,代码实现放在了 https://github.com/drummor/GetHandsDirty
(方案来自淘宝技术团队)
官方出手,官方在 Android N 以上新增了Window.OnFrameMetricsAvailableListener
可以监听每帧的执行状态。包含总耗时,绘制耗时,布局耗时,动画耗时,测量耗时。依次我们可以计算出帧率。
private val metricsAvailableListener =
Window.OnFrameMetricsAvailableListener { window, frameMetrics, dropCountSinceLastInvocation ->
val intent = frameMetrics?.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP) ?: 0
val vsync = frameMetrics?.getMetric(FrameMetrics.VSYNC_TIMESTAMP) ?: 0
val animation = frameMetrics?.getMetric(FrameMetrics.ANIMATION_DURATION) ?: 0
val vsyncTotal = frameMetrics?.getMetric(FrameMetrics.TOTAL_DURATION) ?: 0
val measureCost = frameMetrics?.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION) ?: 0
//计算帧率
}
this.window.addOnFrameMetricsAvailableListener(//向window注册监听
metricsAvailableListener,
Handler(handlerThread.looper)
同时配合 Jetpack 的FrameMetricsAggregator
的可以统计出帧耗时情况。
private val frameMetricsAggregator = FrameMetricsAggregator()
frameMetricsAggregator.add(this@FrameActivity)
frameMetricsAggregator.metrics?.let {
it[FrameMetricsAggregator.TOTAL_INDEX] //总耗时概况
it[FrameMetricsAggregator.INPUT_INDEX] //输入事件耗时
it[FrameMetricsAggregator.DRAW_INDEX] //绘制事件耗时概况
}
FrameMetricsAggregator
内部存储比较有意思,是有一个 SparseIntArray 数组SparseIntArray[] mMetrics = new SparseIntArray[LAST_INDEX + 1]
,存储各个阶段的耗时 SparseIntArray 的 key 为耗时,value 为该耗时的个数。
mMetrics[TOTAL_INDEX]:
{3=8, 4=13, 5=2, 6=44, 7=4, 15=1, 196=1, 198=1, 204=1, 209=1, 210=1, 233=1, 265=1}
如上这是每帧总耗时的分布,耗时 3ms 的有 8 个,耗时 4ms 的有 8 个
我们可以制定自己的标准,诸如单帧耗时<30ms 为优秀,单帧耗时>30ms 且<60ms 为正常,单帧耗时>60ms 且<200ms 为过高,单帧>200 为严重。
首先有一个大的原则,帧耗时统计是在有渲染动作发生时统计,空闲状态不统计。
帧率的统计就是,渲染帧的数量除以有帧渲染发生动作时间得到。
另,每帧的耗时不尽相同,希望抓住主线,针对性的统计慢帧冻帧的数量以及占比。或者切割的更为精细,如 Matrix 里默认的把帧的耗时表现分为四个等级。
再有就是,如通过 adb shell dumpsys gfxinfo packagename 命令或者FrameMetricsAggregator
里的统计方式,把相同耗时的帧进行合并。
帧的统计往往以 page(Activity)为维度,作为一个数据大盘数据。
Window.OnFrameMetricsAvailableListener
与 hook choreograoher 方案对比,Window.OnFrameMetricsAvailableListener
有漏报的情况产生。这需要看 framework 源码进一步追查,有对这方面有研究的同学欢迎留言讨论。