0 介绍
要监控运用界面是否产生卡顿,需求先了解一下Android运用主线程的烘托机制:
Android 体系供给一个安稳的帧率输出机制,让软件层和硬件层能够以共同的频率一起作业,使咱们能够享受安稳帧率的画面。
大部分手机的屏幕都是60Hz的改写率,体系为了配合屏幕的改写频率,每过16.6ms就会发出Vsync信号来通知运用进行制作。假如每个Vsync周期运用都能完结烘托逻辑,那么运用的FPS便是60,给用户的感觉便是十分流通。
在运用层,完成上述机制的关键类便是Choreographer
。每隔16.6ms,Vsync 信号唤醒 Choreographer来做运用的制作操作。
想要监控卡顿或许是监测App的流通度,就必须经过代码手法来获取FPS或许每帧耗时,并转化成能够衡量运用卡顿程度的目标。而几乎所有的卡顿监控计划都离不开Choreographer
这个类。所以先简略说说Choreographer
:
Choreographer
在运用层便是经过Choreographer来承受VSync信号并履行每一帧的烘托逻辑。
每逢Vsync到来时,会往主线程音讯行列里增加一个Message,最终其doFrame函数将被调用:
//Choreographer.java
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
......
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
public void run() {
......
doFrame(mTimestampNanos, mFrame);
}
doFrame函数中履行了运用层的callback,基本上包括了一帧的烘托作业:
//Choreographer.java
void doFrame(long frameTimeNanos, int frame) {
//自带了掉帧核算
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
......
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
......
}
其中CALLBACK_ANIMATION是处理动画相关的逻辑,而CALLBACK_TRAVERSAL则会调用到ViewRootImpl的performTraversals() 函数,然后履行到咱们所了解的View的measure、layout、draw三大流程。
所以能够说,doFrame() 函数包括了运用层制作一帧的逻辑处理。
由于Choreographer处于如此重要的一个方位,基本上所有的卡顿监控都会围绕着Choreographer进行的,除了自带的掉帧核算,Choreographer 供给的 FrameCallback 和 FrameInfo都是运用层能够直接访问的接口。
下面介绍一下市面上开源计划的几种完成方式和简略比照。
1 Choreographer的FrameCallback
TinyDancer 便是经过这种方式核算出FPS。 中心代码:
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (lastFrameTimeNanos > 0) {
val frameTime = (frameTimeNanos - lastFrameTimeNanos) / NANOS_PER_MS
}
lastFrameTimeNanos = frameTimeNanos
Choreographer.getInstance().postFrameCallback(this)
}
})
经过记载doFrame回调的距离时刻作为每帧耗时frameTime,如此也很容易核算出FPS=1000/frameTime,掉帧数=frameTime/16.6。
2 Choreographer + Looper
Tencent/matrix 尽管也是根据Choreographer,但其监测FPS的机制和上面的FrameCallback计划不太相同。
由前面Choreographer的介绍可知,所谓每一帧其实指的便是input、animation、traversal三种事情对应的三个doCallback办法的履行结果,而matrix计算帧耗时便是经过监测这三个办法的履行总时刻来表示。matrix监测FPS的主要完成在LooperMonitor
和UIThreadMoniter
两个类里。
-
LooperMonitor
为主线程Looper设置一个Printer来监听UI线程每个Message的开端、完毕,然后得到Message的履行耗时。(计划同 BlockCanary )class LooperPrinter implements Printer { @Override public void println(String x) { ...... dispatch(x.charAt(0) == '>', x); } private void dispatch(boolean isBegin, String log) { for (LooperDispatchListener listener : listeners) { if (isBegin) { listener.onDispatchStart(log); } else { listener.onDispatchEnd(log); } } } }
2. UIThreadMoniter
经过java反射向Choreographer的CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL三个事情的callback行列的头部插入自定义callback。
```java
//UIThreadMonitor.java
@Override
public void run() {
doFrameBegin(token);
doQueueBegin(CALLBACK_INPUT);
addFrameCallback(CALLBACK_ANIMATION, new Runnable() {
@Override
public void run() {
doQueueEnd(CALLBACK_INPUT);
doQueueBegin(CALLBACK_ANIMATION);
}
}, true);
addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() {
@Override
public void run() {
doQueueEnd(CALLBACK_ANIMATION);
doQueueBegin(CALLBACK_TRAVERSAL);
}
}, true);
}
```
前面提到Choreographer收到VSync信号时,也是往主线程音讯行列里放入一个Message最终触发doFrame。而LooperMonitor监控了每个Message履行的开端/完毕,假如UIThreadMonitor的doFrameBegin
被履行,则阐明当时在 Looper 中正在履行的音讯便是烘托的音讯。然后再在Message完毕的时分作为当时帧制作的完毕。这个整个一帧的监控就闭环了。
所以在Matrix中,完整的一帧耗时是onDispatchStart -> doFrame -> onDispatchEnd
。
由于UIThreadMonitor是在Choreographer的callback行列的头部增加callback,所以可用于记载每种类型callback行列履行的开端时刻,在行列里的callback都履行完毕后,就能够核算对应的事情的耗时(CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL),比方CALLBACK_INPUT耗时
= CALLBACK_ANIMATION开端时刻
– CALLBACK_INPUT开端时刻
。
所以Matrix不只能够得到每一帧的耗时,还能进一步得到每一帧内接触事情、动画、View烘托三种事情的耗时状况。
3 官方计划——JankStats
JankStats —— 是官方推出的Jetpack套件中的一个新库,最早发布于2022年2月,该库供给了在运行时获取界面的每帧性能的回调,能够让开发者监测到性能问题及其产生的原因。
项目中依赖:
dependencies {
implementation "androidx.metrics:metrics-performance:1.0.0-alpha03"
}
简略运用:
val listener = JankStats.OnFrameListener { frameData ->
// A real app could do something more interesting, like writing the info to local storage and later on report it.
Log.v("JankStatsSample", frameData.toString())
}
jankStats = JankStats.createAndTrack(window, listener)
jankStats.isTrackingEnabled = true
简略几行代码就能够在OnFrameListener回调中获取每一帧的性能数据FrameData, 同时JankStats库还供给了PerformanceMetricsState供开发者记载当时的界面状况,并经过FrameData回调出来,有助于了解用户在那一帧期间做了什么交互。
open class FrameData(
frameStartNanos: Long,//当时帧开端时刻
frameDurationUiNanos: Long,//当时帧耗时
isJank: Boolean,//是否产生了卡顿
val states: List<StateInfo>//事务代码记载的UI状况,能够经过PerformanceMetricsState在关键事务代码处埋点
)
原理
JankStats是一个典型的Android X库:在不同的Android版别和不同的设备上,完成行为共同的结构。
class JankStats private constructor(window: Window, private val frameListener: OnFrameListener) {
init {
val decorView: View? = window.peekDecorView()
implementation =
when {
Build.VERSION.SDK_INT >= 31 -> {
JankStatsApi31Impl(this, decorView, window)
}
Build.VERSION.SDK_INT >= 26 -> {
JankStatsApi26Impl(this, decorView, window)
}
Build.VERSION.SDK_INT >= 24 -> {
JankStatsApi24Impl(this, decorView, window)
}
Build.VERSION.SDK_INT >= 22 -> {
JankStatsApi22Impl(this, decorView)
}
Build.VERSION.SDK_INT >= 16 -> {
JankStatsApi16Impl(this, decorView)
}
else -> {
JankStatsBaseImpl(this)
}
}
}
}
阅读源码能够发现监测机制在API 24(7.0)前后有差异:
Android7.0及其以上体系,直接经过 Window 的新办法addOnFrameMetricsAvailableListener
,监听回调每一帧的具体数据FrameMetrics:
internal open class JankStatsApi24Impl() {
@RequiresApi(24)
private fun Window.getOrCreateFrameMetricsListenerDelegator():
DelegatingFrameMetricsListener {
.....
val delegates = mutableListOf<Window.OnFrameMetricsAvailableListener>()
delegator = DelegatingFrameMetricsListener(delegates)
//Window的这个办法能够监听每一帧的具体数据
addOnFrameMetricsAvailableListener(delegator, frameMetricsHandler)
}
return delegator
}
}
Android7.0以下设备,还是需求根据Choreographer,经过反射 Choreographer 的 mLastFrameTimeNanos 来获取当时帧的开端时刻,然后经过 ViewTreeObserver的 OnPreDrawListener来感知制作的开端,并往主线程的音讯行列头部插入runnable来获取当时帧的UI线程制作使命完毕时刻。
//JankStatsApi16Impl.kt
internal open class DelegatingOnPreDrawListener() : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
val frameStart = getFrameStartTime()
with(decorView) {
//往主线程的音讯行列头部插入runnable
handler.sendMessageAtFrontOfQueue(Message.obtain(handler) {
val now = System.nanoTime()
val frameTime = now - frameStart//取得每帧制作的实在耗时
})
}
return true
}
//反射 Choreographer 的 mLastFrameTimeNanos
private fun getFrameStartTime(): Long {
return choreographerLastFrameTimeField.get(choreographer) as Long
}
}
获取到了当时帧的开端时刻和制作的Message完毕时刻,则能够核算出每帧制作的实在耗时frameTime = now – frameStart
计划比照
- Choreographer的FrameCallback
- 长处:
- 简略可靠,维护成本低,运用安稳性高的体系敞开API
- 缺点:
- 调用postFrameCallback()会不断地恳求Vsync(
scheduleVsyncLocked()
),当界面静止时,UI线程也会不断恳求VSync信号。主线程不干活的时分也会监测,或许会把有卡顿的场景的数据给稀释掉了。 - 获取到的帧耗时并不是实在的每帧制作耗时,而获取到≥16ms的两次doFrame的时刻距离。
- 调用postFrameCallback()会不断地恳求Vsync(
- 适用场景:
- 比较粗粒度的掉帧数、卡顿监控。
- 长处:
- Matrix(Choreographer + Looper)
- 长处:
- 能够对UI线程制作的不同阶段耗时进行较详尽的监控
- 不需求postFrameCallback,避免不断恳求VSync信号
- 缺点:
- 维护成本高,对体系api侵入式较强,大量地经过反射来hook Choreographer和Looper,容易呈现兼容问题,高版别体系或许会失效
- 只计算了Choreographer的callback行列履行的耗时,即UI操作相关的耗时,没有包括两个VSYNC之间产生的其它非UI操作相关的message的耗时,因此计算出来的帧耗时或许偏低。
- 适用场景:
- 比较精细化的卡顿监控
- 长处:
- JankStats
- 长处:
- 官方计划,兼容性好,运用简略,可靠安稳。是在体系源码里进行埋点监测并搜集数据。
- 数据详尽,FrameMetrics供给了第三方结构难以收集的具体数据,包括总耗时,input、layout&measure、draw甚至是 sync bitmap到GPU的耗时等等(据说和adb shell dumpsys gfxinfo和GPU呈现模式剖析的数据共同)
- 运用便捷,内置规矩判断当时帧是否卡顿帧,并能够记载当时帧的运用状况。
- 界面中止制作时,不再出产帧率数据,避免脏数据。
- 缺点:
- 不支持个性化的定制需求。
- 库还只是alpha版别,不是老练的release版别,假如有坑则只能等官方更新。
- 集成到老项目时不友好,或许需求晋级AndroidX中心库、kotlin插件甚至gradle插件版别,价值大。
- 适用场景:
- 新项目,需求快速完成卡顿监控
- 长处:
结论
上面三种计划都能够取得当时运用的FPS,要根据项目状况去考虑计划选型。大型项目一般更倾向于维护成本低、安稳性高的根据体系敞开API的计划,所以Choreographer的FrameCallback应该是运用最广泛的计划了。
收集到FPS只是运用卡顿监控的第一步,还需求制定科学的卡顿率目标。由于FPS并不能直观的反映运用的卡顿状况,还需求考虑“视觉惯性”,比方电影帧率仅24FPS也不觉得卡顿,可是运用假如一会30FPS一会60FPS,视觉上就会觉得很不流通。
能够参阅Jank卡顿及stutter卡顿率阐明这篇文档去结合收集到的FPS数据制定卡顿目标,还有matrix的FrameTracer
把帧耗时划分为best\normal\middle\high\frozen几个等级,也是值得借鉴。
demo
todo
参阅
- Android 根据 Choreographer 的烘托机制详解
- Matrix-TraceCanary的规划和原理剖析手册
- JankStats库官方文档