Android渲染系列(3)之Choreographer

本文是Android渲染系列的第三篇

重点介绍 Choreographer,以及 Choreographer 的一些使用场景

🔥交个朋友 加我uestc_xsf(备注进群)

📕 群主旨 :不贩卖焦虑、不卖课、没有星球,都是不收费的,主打一个打工人互帮互助 ,在寒冬一起取暖

👏 Android交流学习、找工作、面试面经、内推等 (群里大佬如云,阿里、字节、支付宝、美团、B站、快手等)

Overview

Google 在 Android 4.1 系统中对 Android Display 系统进行了优化:引入Choreographer给上层 App 的渲染提供一个稳定的 Message 处理的时机

也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机。具体来说一旦收到 VSync 通知,CPU 和 GPU 就立刻开始计算,然后把数据写入 Buffer。

比如在 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms , Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 。

  • Choreographer,意为舞蹈编导、编舞者。在这里就是指对 CPU/GPU 绘制的指导 ------ 收到 VSync 信号才开始绘制,保证绘制拥有完整的 16.6ms,避免绘制的随机性(确切的来说是Vsync_APP)

Choreographer,是一个 Java 类,包路径 android.view.Choreographer。类注释是 "协调动画、输入和绘图的计时",通常应用层不会直接使用 Choreographer,而是使用更高级的 API,例如动画和 View 绘制相关的 ValueAnimator.start()、View.invalidate() 等。

  • 通常我们也可以 Choreographer 来监控应用的帧率

Choreographer登场

首先 Android 主线程负责渲染UI,其运行的本质其实就是Handler处理 Message 的过程

我们的各种操作,包括每一帧的渲染操作 ,都是通过 Message 的形式发给主线程的 MessageQueue ,MessageQueue 处理完消息继续等下一个消息

引入 Vsync 之前的 Android 版本,渲染一帧相关的 Message ,中间是没有间隔的,上一帧绘制完,下一帧的 Message 紧接着就开始被处理。这样的问题就是,帧率不稳定,可能高也可能低,不稳定,可以看之前的Android渲染之Vsync文章。

对于用户来说,稳定的帧率才是好的体验,比如你玩吃鸡,相比 fps 在 40 和 60 之间频繁变化,用户感觉更好的是稳定在 50 fps 的情况. 所以 Android 的演进中,4.1系统中引入了 Vsync + TripleBuffer + Choreographer 的机制,其主要目的就是提供一个稳定的帧率输出机制,让软件层和硬件层可以以共同的频率一起工作。

作用

Choreographer 和 Vsync 共同解决生产者何时生产Buffer,消费者何时消费Buffer的问题。 试想一下这个情况:

  • 假如生产者和消费者的行为都是非常激进的,两者会轮流做生产和消费:生产者先生产,然后把 Buffer 放回到 BufferQueue 后消费者马上消费,然后生产者又马上拿出来生产,两者的行为不会有时间间隙,会持续这样进行。

这里看起来有没什么大问题呢?有。目前手机屏幕的刷新率在 60Hz ~ 120Hz 之间。也就是一秒时间内,最快也就刷新 120 次,要想产生让用户感到流畅的画面,一秒生产 / 消费 120 次就足够了,但如果以上述激进的方式来生产和消费,那么可能会产出很多无用功,带来功耗的提升和发热。 因此需要一个「协调者」来协调这个工作。 Android 这里的协调者就是 Choreographer------a person who composes the sequence of steps and moves for a performance of dance. Choreographer 协调生产者什么时候去生产------也就是什么时候去绘制一帧。既然要协调,那么肯定是需要有一个协调的依据,这个依据就是 Vsync 信号------也就是垂直同步信号

这里再次强调一下

Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机.

至于为什么 Vsync 周期选择是 16.6ms (60 fps) ,是因为目前大部分手机的屏幕都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每隔 16.6 ms ,

Vsync 信号到来唤醒 Choreographer 来做 App 的绘制操作 ,如果每个 Vsync 周期应用都能渲染完成,那么应用的 fps 就是 60 ,给用户的感觉就是非常流畅,这就是引入 Choreographer 的主要作用

Choreographer 扮演 Android 渲染链路中承上启下的角色

  1. 承上:负责接收和处理 App 的各种更新消息和回调,等到 Vsync 到来的时候统一处理。比如集中处理 Input(主要是 Input 事件的处理) 、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操作) ,判断卡顿掉帧情况,记录 CallBack 耗时等
  2. 启下:负责请求和接收 Vsync 信号。接收 Vsync 事件回调(通过 FrameDisplayEventReceiver.onVsync );请求 Vsync(FrameDisplayEventReceiver.scheduleVsync)

工作流程

  1. Choreographer 初始化
  2. 初始化 FrameHandler ,绑定 Looper
  3. 初始化 FrameDisplayEventReceiver ,与 SurfaceFlinger 建立通信用于接收和请求 Vsync
  4. 初始化 CallBackQueues
  5. SurfaceFlinger 的 appEventThread 唤醒发送 Vsync ,Choreographer 回调 FrameDisplayEventReceiver.onVsync , 进入 Choreographer 的主处理函数 doFrame
  6. Choreographer.doFrame 计算掉帧逻辑
  7. Choreographer.doFrame 处理 Choreographer 的第一个 callback : input
  8. Choreographer.doFrame 处理 Choreographer 的第二个 callback : animation
  9. Choreographer.doFrame 处理 Choreographer 的第三个 callback : insets animation
  10. Choreographer.doFrame 处理 Choreographer 的第四个 callback : traversal
  11. traversal-draw 中 UIThread 与 RenderThread 同步数据
  12. Choreographer.doFrame 处理 Choreographer 的第五个 callback : commit
  13. RenderThread 处理绘制命令,将处理好的绘制命令发给 GPU 处理
  14. 调用 swapBuffer 提交给 SurfaceFlinger 进行合成

此时 Buffer 并没有真正完成,需要等 CPU 完成后 SurfaceFlinger 才能真正使用,新版本的 Systrace 中有 gpu 的 fence 来标识这个时间

核心源码

这部分可以看下gityuan的源码分析网上源码分析这块也比较多 gityuan.com/2017/02/25/...

源码一些要点总结

  1. Choreographer 是线程单例的,管理消息处理,因此必须要和一个 Looper 绑定,因为其内部有一个 Handler 需要和 Looper 绑定,一般是 App 主线程的 Looper 绑定
  2. DisplayEventReceiver 是一个 abstract class,其 JNI 的代码部分会创建一个IDisplayEventConnection 的 Vsync 监听者对象。这样,来自 AppEventThread 的 VSYNC 中断信号就可以传递给 Choreographer 对象了。当 Vsync 信号到来时,DisplayEventReceiver 的 onVsync 函数将被调用。
  3. DisplayEventReceiver 还有一个 scheduleVsync 函数。当应用需要绘制UI时,将首先申请一次 Vsync 中断,然后再在中断处理的 onVsync 函数去进行绘制。
  4. Choreographer 定义了一个 FrameCallback interface,每当 Vsync 到来时,其 doFrame 函数将被调用。这个接口对 Android Animation 的实现起了很大的帮助作用。以前都是自己控制时间,现在终于有了固定的时间中断

这个FrameCallback接口通常在应用可以做卡顿检测

  1. Choreographer 的主要功能是,当收到 Vsync 信号时,去调用使用者通过 postCallback 设置的回调函数。目前一共定义了五种类型的回调,它们分别是:
    1. CALLBACK_INPUT : 处理输入事件处理有关
    2. CALLBACK_ANIMATION : 处理 Animation 的处理有关
    3. CALLBACK_INSETS_ANIMATION : 处理 Insets Animation 的相关回调
    4. CALLBACK_TRAVERSAL : 处理和 UI 等控件绘制有关
    5. CALLBACK_COMMIT : 处理 Commit 相关回调,主要是是用于执行组件 Application/Activity/Service 的 onTrimMemory,在 ApplicationThread 的 scheduleTrimMemory 方法中向 Choreographer 插入的;另外这个 Callback 也提供了一个监测一帧耗时的时机
  2. ListView 的 Item 初始化(obtain\setup) 会在 input 里面也会在 animation 里面,这取决于
  3. CALLBACK_INPUTCALLBACK_ANIMATION 会修改 view 的属性,所以要先与 CALLBACK_TRAVERSAL 执行

一些使用场景

在了解了Choreographer的工作原理之后,我们来点实际的,将Choreographer这块的知识利用起来。它可以帮助我们检测应用的fps或者卡顿

检测FPS

有了上面的分析,我们知道Choreographer内部去监听了VSYNC信号,并且当VSYNC信号来临时会发个异步消息给Looper,在执行到这个消息时会通知外部观察者(上面的观察者就是ViewRootImpl),通知ViewRootImpl可以开始绘制了。 Choreographer的每次回调都是在通知观察者,可以进行绘制了,我们只需要统计出1秒内这个回调次数有多少次,即可知道是多少fps。 反Choreographer提供了另外一个postFrameCallback方法。我看了下源码,与postCallback差异不大,只不过注册的观察者类型是CALLBACK_ANIMATION,但这不影响它回调

scss 复制代码
//Choreographer.java
public void postFrameCallback(FrameCallback callback) {
    postFrameCallbackDelayed(callback, 0);
}
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
    postCallbackDelayedInternal(CALLBACK_ANIMATION,
            callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
kotlin 复制代码
object FpsMonitor {

    private const val FPS_INTERVAL_TIME = 1000L

    /**
     * 1秒内执行回调的次数  即fps
     */
    private var count = 0
    private val mMonitorListeners = mutableListOf<(Int) -> Unit>()

    @Volatile
    private var isStartMonitor = false
    private val monitorFrameCallback by lazy { MonitorFrameCallback() }
    private val mainHandler by lazy { Handler(Looper.getMainLooper()) }

    fun startMonitor(listener: (Int) -> Unit) {
        mMonitorListeners.add(listener)
        if (isStartMonitor) {
            return
        }
        isStartMonitor = true
        Choreographer.getInstance().postFrameCallback(monitorFrameCallback)
        //1秒后结算 count次数
        mainHandler.postDelayed(monitorFrameCallback, FPS_INTERVAL_TIME)
    }

    fun stopMonitor() {
        isStartMonitor = false
        count = 0
        Choreographer.getInstance().removeFrameCallback(monitorFrameCallback)
        mainHandler.removeCallbacks(monitorFrameCallback)
    }

    class MonitorFrameCallback : Choreographer.FrameCallback, Runnable {

        //VSYNC信号到了,且处理到当前异步消息了,才会回调这里
        override fun doFrame(frameTimeNanos: Long) {
            //次数+1  1秒内
            count++
            //继续下一次 监听VSYNC信号
            Choreographer.getInstance().postFrameCallback(this)
        }

        override fun run() {
            //将count次数传递给外面
            mMonitorListeners.forEach {
                it.invoke(count)
            }
            count = 0
            //继续发延迟消息  等到1秒后统计count次数
            mainHandler.postDelayed(this, FPS_INTERVAL_TIME)
        }
    }

}

通过记录每秒内Choreographer回调的次数,即可得到FPS

检测卡顿

卡顿检测通常有如下2种方式

  • 1、Choreographer

通过设置Choreographer的FrameCallback,可以在每一帧被渲染的时候记录下它开始渲染的时间,这样在下一帧被处理时,我们可以根据时间差来判断上一帧在渲染过程中是否出现掉帧。Android中,每发出一个VSYNC信号都会通知界面进行重绘、渲染,每一次同步周期为16.6ms,代表一帧的刷新频率。每次需要开始渲染的时候都会回调doFrame(),如果某2次doFrame()之间的时间差大于16.6ms,则说明发生了UI有点卡顿,已经在掉帧了,拿着这个时间差除以16.6就得出了掉过了多少帧。

kotlin 复制代码
object ChoreographerMonitor {
    @Volatile
    private var isStart = false
    private val monitorFrameCallback by lazy { MonitorFrameCallback() }
    private var mListener: (Int) -> Unit = {}
    private var mLastTime = 0L

    fun startMonitor(listener: (Int) -> Unit) {
        if (isStart) {
            return
        }
        mListener = listener
        Choreographer.getInstance().postFrameCallback(monitorFrameCallback)
        isStart = true
    }

    fun stopMonitor() {
        isStart = false
        Choreographer.getInstance().removeFrameCallback { monitorFrameCallback }
    }

    class MonitorFrameCallback : Choreographer.FrameCallback {

        private val refreshRate by lazy {
            //计算刷新率 赋值给refreshRate
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                App.getAppContext().display?.refreshRate ?: 16.6f
            } else {
                val windowManager =
                    App.getAppContext().getSystemService(Context.WINDOW_SERVICE) as WindowManager
                windowManager.defaultDisplay.refreshRate
            }
        }

        override fun doFrame(frameTimeNanos: Long) {
            mLastTime = if (mLastTime == 0L) {
                frameTimeNanos
            } else {
                //frameTimeNanos的单位是纳秒,这里需要计算时间差,然后转成毫秒
                val time = (frameTimeNanos - mLastTime) / 1000000
                //跳过了多少帧
                val frames = (time / (1000f / refreshRate)).toInt()
                if (frames > 1) {
                    mListener.invoke(frames)
                }
                frameTimeNanos
            }
            Choreographer.getInstance().postFrameCallback(this)
        }

    }
}

因为postFrameCallback()方法只能监听一次VSYNC信号,所以doFrame()都得再次调用,继续监听下一次VSYNC信号。 这种方案适合监控线上环境的app掉帧情况来计算app在某些场景的流畅度,然后有针对地做性能优化。 检测卡顿除了上述方案日常还有

  • 2、Looper+Printer检测卡顿

这里介绍另一种方式来进行卡顿检测(这里指主线程,子线程一般不关心卡顿问题)--Looper。先来看一段loop代码

java 复制代码
//Looper.java
private Printer mLogging;
public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}

public static void loop() {
    final Looper me = myLooper();
    for (;;) {
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        ...
        msg.target.dispatchMessage(msg);
        ...
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
    }
}

从这段代码可以看出,如果我们设置了Printer,那么在每个消息分发的前后都会打印一句日志来标识事件分发的开始和结束。这个点可以利用一下,我们可以通过Looper打印日志的时间间隔来判断是否发生卡顿,如果发生卡顿,则将此时线程的堆栈信息给保存下来,进而分析哪里卡顿了。这种匹配字符串方案能够准确地在发生卡顿时拿到堆栈信息。 工具类如下

kotlin 复制代码
const val TAG = "looper_monitor"

/**
 * 默认卡顿阈值
 */
const val DEFAULT_BLOCK_THRESHOLD_MILLIS = 3000L
const val BEGIN_TAG = ">>>>> Dispatching"
const val END_TAG = "<<<<< Finished"

class LooperPrinter : Printer {

    private var mBeginTime = 0L

    @Volatile
    var mHasEnd = false
    private val collectRunnable by lazy { CollectRunnable() }
    private val handlerThreadWrapper by lazy { HandlerThreadWrapper() }

    override fun println(msg: String?) {
        if (msg.isNullOrEmpty()) {
            return
        }
        log(TAG, "$msg")
        if (msg.startsWith(BEGIN_TAG)) {
            mBeginTime = System.currentTimeMillis()
            mHasEnd = false

            //需要单独搞个线程来获取堆栈
            handlerThreadWrapper.handler.postDelayed(
                collectRunnable,
                DEFAULT_BLOCK_THRESHOLD_MILLIS
            )
        } else {
            mHasEnd = true
            if (System.currentTimeMillis() - mBeginTime < DEFAULT_BLOCK_THRESHOLD_MILLIS) {
                handlerThreadWrapper.handler.removeCallbacks(collectRunnable)
            }
        }
    }

    fun getMainThreadStackTrace(): String {
        val stackTrace = Looper.getMainLooper().thread.stackTrace
        return StringBuilder().apply {
            for (stackTraceElement in stackTrace) {
                append(stackTraceElement.toString())
                append("\n")
            }
        }.toString()
    }

    inner class CollectRunnable : Runnable {
        override fun run() {
            if (!mHasEnd) {
                //主线程堆栈给拿出来,打印一下
                log(TAG, getMainThreadStackTrace())
            }
        }
    }

    class HandlerThreadWrapper {
        var handler: Handler
        init {
            val handlerThread = HandlerThread("LooperHandlerThread")
            handlerThread.start()
            handler = Handler(handlerThread.looper)
        }
    }

}

主要思路就是在println()回调时判断回调的文本信息是开始还是结束。如果是开始则搞个定时器,3秒后就认为是卡顿,就开始取主线程堆栈信息输出日志,如果在这3秒内消息已经分发完成,那么就不是卡顿,就把这个定时器取消掉。

参考

相关推荐
工业甲酰苯胺1 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3431 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee2 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯3 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
uzong4 小时前
7 年 Java 后端,面试过程踩过的坑,我就不藏着了
java·后端·面试
Jinkey4 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!6 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟7 小时前
Android音频采集
android·音视频
小白也想学C8 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程8 小时前
初级数据结构——树
android·java·数据结构