Matrix 帧率统计

/ 0评 / 28

Matrix

在最新版本的Matrix上,已经放弃以前的UIThreadMonitor,改用com.tencent.matrix.trace.tracer.FrameTracer来监听帧率

OnFrameMetricsAvailableListener

在 Android 7.0, 增加了一个 Api 可以方便的统计一个 Window 的 View 树的绘制耗时。

FrameMetrics 指标 说明
ANIMATION_DURATION 表示执行动画回调的耗时
COMMAND_ISSUE_DURATION 表示向 GPU 发出绘制命令的耗时
DRAW_DURATION 表示将 View 树转换为 DisplayList 的耗时
FIRST_DRAW_FRAME 表示当前帧是否是当前 Window 布局中绘制的第一帧
INPUT_HANDLING_DURATION 表示处理输入事件回调的耗时
INTENDED_VSYNC_TIMESTAMP 当前帧的预期开始时间, 如果此值与 VSYNC_TIMESTAMP 不同,则表示 UI 线程上发生了阻塞,阻止了 UI 线程及时响应vsync信号
LAYOUT_MEASURE_DURATION 表示对 View 树进行 measure 和 layout 所花的时间
SWAP_BUFFERS_DURATION 表示将此帧的帧缓冲区发送给显示子系统所花的时间
SYNC_DURATION 表示将 DisplayList 与渲染线程同步所花的时间
TOTAL_DURATION 表示此帧渲染并发布给显示子系统所花费的总时间, 等于所有其他具有时间价值的指标的值之和
UNKNOWN_DELAY_DURATION 表示等待 UI 线程响应并处理帧所经过的时间, 大多数情况下应为0
VSYNC_TIMESTAMP 在所有 vsync 监听器和帧绘制中使用的时间值(Choreographer 的帧回调, 动画, View#getDrawingTime等)

以上所有单位为纳秒,以上所有指标都存储在一个 long 类型的数组中, 使用 FrameMetrics#getMetric 方法可以从中提取。

使用

@RequiresApi(Build.VERSION_CODES.O)
private fun initFrameMetricsListener() {
    val thread = HandlerThread("frame-stat").apply { start() }
    val handler = Handler(thread.looper)
    window.addOnFrameMetricsAvailableListener(object : Window.OnFrameMetricsAvailableListener {

        override fun onFrameMetricsAvailable(
            window: Window?, metric: FrameMetrics?, dropCountSinceLastInvocation: Int
        ) {
            // 会在 handler 对应的 thread 中执行
            val frameMetricsCopy = FrameMetrics(metric) /* 注意需要做深拷贝, 再使用 */
            val vsyncTimestamp = frameMetricsCopy.getMetric(FrameMetrics.VSYNC_TIMESTAMP)
            val intendedVsyncTimestamp =
                frameMetricsCopy.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP)
            val totalDuration: Long = frameMetricsCopy.getMetric(FrameMetrics.TOTAL_DURATION)
            Log.d("FrameStat","is first frame: ${frameMetricsCopy.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1L}")
            Log.d("FrameStat","measure layout: ${frameMetricsCopy.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION) / 1000000} ms")
            Log.d("FrameStat","draw: ${frameMetricsCopy.getMetric(FrameMetrics.DRAW_DURATION) / 1000000} ms")
            Log.d("FrameStat", "total: ${totalDuration / 1000000} ms")
            Log.d("FrameStat", "delay draw: ${vsyncTimestamp != intendedVsyncTimestamp}")

            //获取屏幕刷新率
            updateRefreshRate(window)
            //获取每一帧耗时
            val frameIntervalNanos: Float = TIME_SECOND_TO_NANO / cachedRefreshRate
            //计算本帧一共掉了多少帧
            val droppedFrames =
                Math.max(0f, (totalDuration - frameIntervalNanos) / frameIntervalNanos)

            if (droppedFrames > 0) {
                Log.d("FrameStat", "droppedFrames = $droppedFrames")
            }
            Log.d("FrameStat", "=============")
        }

        private val TIME_SECOND_TO_NANO = 1000000000
        private var cachedRefreshRate: Float = 60f
        private var lastModeId = -1
        private var attributes: WindowManager.LayoutParams? = null

        //更新屏幕刷新率
        private fun updateRefreshRate(window: Window?) {
            if (attributes == null) {
                attributes = window?.attributes
            }
            attributes?.let {
                if (it.preferredDisplayModeId != lastModeId) {
                    lastModeId = it.preferredDisplayModeId
                    cachedRefreshRate = getRefreshRate(window)
                }
            }
        }

        private fun getRefreshRate(window: Window?): Float {
            if (window == null) {
                return cachedRefreshRate
            }
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                window.context.display!!.refreshRate
            } else {
                window.windowManager.defaultDisplay.refreshRate
            }
        }
    }, handler)
}

一些注意点

1、dropCountSinceLastInvocation:方法注释里面说这个值为从上一次回调开始掉帧数,不过一直为0

2、虽然注释说INTENDED_VSYNC_TIMESTAMP与VSYNC_TIMESTAMP不一致,则代表ui线程出现阻塞,但是在Touch里面耗时,这个为一致的,推荐使用TOTAL_DURATION来计算帧率

FrameMetricsAggregator

如果我们使用addOnFrameMetricsAvailableListener,但是其实我们可以使用官方提供的FrameMetricsAggregator

private val frameMetricsAggregator = FrameMetricsAggregator()

override fun onResume() {
    frameMetricsAggregator.add(this)
    super.onResume()
}

override fun onPause() {
    Log.i("FrameStat","onPause:  ${FrameMetricsCalculator.calculateFrameMetrics(frameMetricsAggregator.metrics)}")
    frameMetricsAggregator.remove(this)
    frameMetricsAggregator.reset()
    super.onPause()
}

override fun onDestroy() {
    frameMetricsAggregator.stop()
    super.onDestroy()
}

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}

计算帧率辅助类

object FrameMetricsCalculator {

    fun calculateFrameMetrics(arr: Array<SparseIntArray?>?): PerfFrameMetrics {
        var totalFrames = 0
        var slowFrames = 0
        var frozenFrames = 0
        if (arr != null) {
            val frameTimes = arr[FrameMetricsAggregator.TOTAL_INDEX]
            if (frameTimes != null) {
                for (i in 0 until frameTimes.size()) {
                    val frameTime = frameTimes.keyAt(i)
                    val numFrames = frameTimes.valueAt(i)
                    totalFrames += numFrames
                    if (frameTime > 700) {
                        frozenFrames += numFrames
                    }
                    if (frameTime > 16) {
                        slowFrames += numFrames
                    }
                }
            }
        }
        return PerfFrameMetrics(totalFrames, slowFrames, frozenFrames)
    }

    data class PerfFrameMetrics(
        val totalFrames: Int,
        val slowFrames: Int,
        val frozenFrames: Int
    ) {
        fun deltaFrameMetricsFromSnapshot(that: PerfFrameMetrics): PerfFrameMetrics {
            val newTotalFrames = totalFrames - that.totalFrames
            val newSlowFrames = slowFrames - that.slowFrames
            val newFrozenFrames = frozenFrames - that.frozenFrames
            return PerfFrameMetrics(newTotalFrames, newSlowFrames, newFrozenFrames)
        }
    }
}

监听主线程耗时

使用Looper#Printer也可以监听耗时,如下所示

private fun initLooperListener() {
    Looper.getMainLooper().setMessageLogging(object : Printer {
        private var isStart = false
        private var startNs = 0L

        override fun println(x: String?) {
            x?.let {
                if (it[0] == '>') {
                    isStart = true
                    startNs = System.nanoTime()
                } else {
                    if (isStart) {
                        Log.i("FrameStat", "const = ${(System.nanoTime() - startNs) / 1000000f}")
                    }
                }
            }
        }

    })
}

参考文档

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注