0 介绍

要监控运用界面是否产生卡顿,需求先了解一下Android运用主线程的烘托机制:
Android 体系供给一个安稳的帧率输出机制,让软件层和硬件层能够以共同的频率一起作业,使咱们能够享受安稳帧率的画面。
大部分手机的屏幕都是60Hz的改写率,体系为了配合屏幕的改写频率,每过16.6ms就会发出Vsync信号来通知运用进行制作。假如每个Vsync周期运用都能完结烘托逻辑,那么运用的FPS便是60,给用户的感觉便是十分流通。
在运用层,完成上述机制的关键类便是Choreographer。每隔16.6ms,Vsync 信号唤醒 Choreographer来做运用的制作操作。
想要监控卡顿或许是监测App的流通度,就必须经过代码手法来获取FPS或许每帧耗时,并转化成能够衡量运用卡顿程度的目标。而几乎所有的卡顿监控计划都离不开Choreographer这个类。所以先简略说说Choreographer

Choreographer

常见的Android应用卡顿监控方案原理和对比

在运用层便是经过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() 函数包括了运用层制作一帧的逻辑处理。

常见的Android应用卡顿监控方案原理和对比
由于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的主要完成在LooperMonitorUIThreadMoniter两个类里。

  1. 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开端时刻

常见的Android应用卡顿监控方案原理和对比
所以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

计划比照

  1. Choreographer的FrameCallback
    • 长处:
      • 简略可靠,维护成本低,运用安稳性高的体系敞开API
    • 缺点:
      • 调用postFrameCallback()会不断地恳求Vsync(scheduleVsyncLocked()),当界面静止时,UI线程也会不断恳求VSync信号。主线程不干活的时分也会监测,或许会把有卡顿的场景的数据给稀释掉了。
      • 获取到的帧耗时并不是实在的每帧制作耗时,而获取到≥16ms的两次doFrame的时刻距离。
    • 适用场景:
      • 比较粗粒度的掉帧数、卡顿监控。
  2. Matrix(Choreographer + Looper)
    • 长处:
      • 能够对UI线程制作的不同阶段耗时进行较详尽的监控
      • 不需求postFrameCallback,避免不断恳求VSync信号
    • 缺点:
      • 维护成本高,对体系api侵入式较强,大量地经过反射来hook Choreographer和Looper,容易呈现兼容问题,高版别体系或许会失效
      • 只计算了Choreographer的callback行列履行的耗时,即UI操作相关的耗时,没有包括两个VSYNC之间产生的其它非UI操作相关的message的耗时,因此计算出来的帧耗时或许偏低。
    • 适用场景:
      • 比较精细化的卡顿监控
  3. JankStats
    • 长处:
      • 官方计划,兼容性好,运用简略,可靠安稳。是在体系源码里进行埋点监测并搜集数据。
      • 数据详尽,FrameMetrics供给了第三方结构难以收集的具体数据,包括总耗时,input、layout&measure、draw甚至是 sync bitmap到GPU的耗时等等(据说和adb shell dumpsys gfxinfoGPU呈现模式剖析的数据共同)
      • 运用便捷,内置规矩判断当时帧是否卡顿帧,并能够记载当时帧的运用状况。
      • 界面中止制作时,不再出产帧率数据,避免脏数据。
    • 缺点:
      • 不支持个性化的定制需求。
      • 库还只是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

参阅

  1. Android 根据 Choreographer 的烘托机制详解
  2. Matrix-TraceCanary的规划和原理剖析手册
  3. JankStats库官方文档