作者:Drummor
帧率(Frame rate)是以帧称为单位的
位图
图像连续出现在显示器上的频率(速率)。
线下开发我们可以使用开发者选项的帧率监控或者 adb shell dumpsys gfxinfo packagename
进行监控针对性优化。这些方案不能带到线上。
利用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
这部分。#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
}
}
}
这样我们就实现了一个监控滑动帧率的方案
官方出手,官方在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)为维度,作为一个数据大盘数据。
我们在对产品进行性能监控时,有时会遇到一些性能相关问题,这时我们就需要进行相关的优化,而在优化的同时,你会发现中间有些问题设计到Framework 底层知识点。但大多数Android开发者基本做的是业务开发,对Framework 底层的知识点的了解知识表面的,如果深层次点的就不行了。所以为了弥补大家在这一块知识点的空缺,为大家准备了相关联的学习笔记进行参考学习,有:《Android Framework 底层原理知识点笔记》、《Android 性能优化知识点汇总》、《Android 性能监控框架》……等