前言
Android 流畅性监控的三板斧,这里所指是【帧率的监控】,【卡顿监控】和【ANR的监控】。之所以讲这三者放在一同是他们的联络比较亲近。帧率的下降往往伴随着有卡顿,【过分卡顿】往往就会发生ANR。
谨慎的讲,帧率下降不一定会有卡顿(这里对卡顿是从技能视点定义在主线程履行了耗时使命),卡顿发生的原因还有其他因素导致,比方系统负载、CPU繁忙等。关于卡顿的具体内容放在流畅性三板斧的第二篇。
【过分的卡顿】也不一定发生ANR,卡顿但未触发ANR发生的条件就不会发生ANR。关于ANR的具体内容咱们放在三板斧系列文章的第三篇。
Android 流畅性三板斧之帧率监控(/post/721780…)
Android 流畅性三板斧之卡顿监控(/post/721877…)
Android 流畅性三板斧之ANR监控(即将发布)
该篇咱们从运用开发者的视点,探索在运用层监控帧率的四种办法。
温馨提示,本文触及的完结的代码以上传至github github.com/drummor/Get…,结合代码食用更佳
1 什么是帧率
帧率(Frame rate)是以帧称为单位的位图图画连续出现在显示器上的频率(速率)。
2 Android 中帧率的监控
线下开发咱们能够运用开发者选项的帧率监控或许 adb shell dumpsys gfxinfo packagename
进行监控针对性优化。这些计划不能带到线上。
惯常咱们在Android里线下对帧率的监控首要依托Choreographer,关于Choreographer不再赘述在其他的文章有比较全面的介绍能够看这两篇文章
- /post/684490…
- /post/720986…
3 简略监控帧率计划
运用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);
}
})
}
}
长处
- 简略快捷,无黑科技
缺陷
- 无活动时,也会监控,无效信息会把帧率较低时给平均掉。
- 对运用带来不必要的担负。
4 帧率监控进化之一 hook Choreographer
针对章节三的计划,首要咱们有两个首要的优化方向期望在主线程不活动的时分不进行帧率的检测
咱们调用揭露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);
}
}
- 【收集每帧的开端】运用Looper中Printer收集Message的开端和完毕。上段代码是
Choreographer
中的一段代码。当收到底层笔直同步信号的时,运用Handler机制post的一个Runable,履行该帧的动作doFrame()
。顺次咱们能够收集到每帧的开端和完毕。
# Choreographer
private final CallbackQueue[] mCallbackQueues;
- 【过滤出每帧的履行动作】咱们知道主线程中不单单履行每帧的动作,还会履行其他动作。如何过滤出履行的是每帧的动作。反射往Choreographer往里添加callback不触发笔直同步,一起在同步信号回调时,会调用咱们传入的callback,假如履行了传入的callbacl就能够标识该次履行动作是帧的履行动作。
- 【收集实在的笔直同步到达时刻】反射拿到
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);
...
}
- 一起咱们还能够经过反射的办法给Chorographer 里 mCallbackQueues添加不同的类型动作,收集不同类型动作的耗时。
补充
- 严厉意义上,该计划核算的也不是实在的帧率,而是一帧一切耗时中在UI Thread履行部分的耗时,上图
doFrame
部分。其他线程和进程还会履行其他动作终究才干完结一帧的制作。但对于咱们运用层来说更重视监控doFrame
,咱们在运用开发层面大部分能够干涉的也在doFrame
这部分。
(计划思路Matrix)
关于这个计划可查看: github.com/drummor/Get…
5 帧率监控进化之二 滑动帧率
#View
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
...
final AttachInfo ai = mAttachInfo;
if (ai != null) {
ai.mViewScrollChanged = true;
}
...
}
- View里假如有滑动行为发生终究都会调用到
onScrollChanged()
,当该办法调用的时分,会将mAttachInfo的mViewScrollChanged值设为true
#ViewRootImpl
private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
...
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}
}
- 如上代码ViewRootImpl的draw办法会假如check到
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 } } }
这样咱们就完结了一个监控滑动帧率的计划,代码完结放在了 github.com/drummor/Get…
(计划来自淘宝技能团队)
6 帧率监控进化 之三 官方计划
官方出手,官方在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为严重。
7 数据核算
首要有一个大的原则,帧耗时核算是在有烘托动作发生时核算,空闲状况不核算。
帧率的核算便是,烘托帧的数量除以有帧烘托发生动作时刻得到。
另,每帧的耗时不尽相同,期望抓住主线,针对性的核算慢帧冻帧的数量以及占比。或许切割的更为精细,如Matrix里默认的把帧的耗时体现分为四个等级。
- 正常帧,<3*16ms
- 中心帧,<9*16ms
- 慢帧,<24*16ms
- 冻帧,<42*16ms
再有便是,如经过adb shell dumpsys gfxinfo packagename命令或许FrameMetricsAggregator
里的核算办法,把相同耗时的帧进行合并。
帧的核算往往以page(Activity)为维度,作为一个数据大盘数据。
8 其他
- 帧率实在一个抽象的目标,会存在单帧耗时很高,仍是帧率平均下来很优异,从数据上看问题不大,但是用户的感知会比较强烈。咱们更需要做的找到那个隐藏着的【耗时高】的单帧;咱们需要全面的对主线程里的履行使命进行全面的监控,也便是卡顿监控的范畴。
- 帧率仅仅核算【页面制作】的概略,不能够全面反映主线程的耗时状况。主线程假如存在耗时动作,比方一个主线程的Handler的履行了一个>100ms的使命,假如此刻并没有制作使命需要履行,此刻的不一定帧率就会降低。
- 【warning!!】最后,已经困扰好几天,实践测试中发现,运用
Window.OnFrameMetricsAvailableListener
与hook choreograoher计划对比,Window.OnFrameMetricsAvailableListener
有漏报的状况发生。这需要看framework源码进一步清查,有对这方面有研究的同学欢迎留言讨论。 - 本文触及的完结的代码以上传至github github.com/drummor/Get…
重视点赞鼓励,流畅性三板斧系列剩下的两篇,卡顿监控和ANR监控也会陆续放出。
本文正在参与「金石计划」