Android 屏幕刷新机制

前言

在上一篇 View的工作流程 的博客中,分析了ViewRootImpl类中应用窗口 measure,layout 和 draw 的过程。今天这篇文章探索从ViewRootImpl 到屏幕的刷新之间的渊源。



View的工作流程

//ViewRootImpl 


    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();//检查是否在主线程
            mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
            scheduleTraversals();
        }
    }


    void scheduleTraversals() {
        if (!mTraversalScheduled) {//同一帧内不会多次调用遍历
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//拦截同步Message

            //探索的入口
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

        }
    }


    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();



    void doTraversal() {
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);//移除同步消息屏障

            performTraversals();//View的绘制流程正式开始。
    }



针对以下 2 个要点分析

  • ① postSyncBarrier : Handler 的同步屏障
    简单来说,同步屏障的作用是可以拦截 Looper 对同步消息的获取和分发,因为 Handler 消息机制中,Looper 会不断的从 MessageQueue 中取出 Message。加入同步屏障之后,Looper 只会获取和处理异步消息,如果没有异步消息那么就会进入阻塞状态。
    这么做的原因是 View 的绘制和屏幕刷新优先级肯定是最高的(也就是 VIP ,防止卡顿), 除了对View绘制渲染的处理操作可以优先处理(设置为异步消息),其它的 Message 都可以放置一边。保障弱势群体的权益。

  • ① Choreographer : 编舞者。(系统工程师认为 View的渲染绘制就是指尖上的艺术,每一次交互都是一场视觉盛宴,创作过程中增添几分意境)



Choreographer

Choreographer 是 Jelly Bean(Android 4.1)中黄油计划(Project Butter)引入的产物,包括 :

  • Choreographer : 负责统一动画、输入和绘制时机。
  • VSYNC : 垂直同步信号。
  • Triple Buffer : 第三块绘制Buffer,减少显示内容的延迟。


ViewRootImpl中,scheduleTraversals()方法调用Choreographer 的 postCallback() 方法传入将要执行遍历绘制的 runnable。

也可以这么说 : ViewRootImpl 的遍历绘制doTraversal()方法,由编舞者 Choreographer 主导,在时机成熟的时候,Choreographer 会回调callback方法,View开始遍历绘制 measure –> layout –> draw 。

//Choreographer 

    //Posts a callback to run on the next frame.  也就是绘制下一帧的内容
    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);//延迟时间为0
    }

    public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
        ``````
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }

    private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) {

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;

            //根据时间将 action 添加到 mCallbackQueue 的队列中
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                //因为传入的延迟时间delayMillis 为 0
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                //设置异步延迟消息 ,过dueTime后执行(无视同步屏障)
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

postCallbackDelayedInternal()方法中,我们注意到 :

  • mCallbackQueues : callback回调queue会根据时间添加入action(也就是触发doTraversal()方法的那个runnable)。在时机成熟的时候,mCallbackQueues 会回调这些 action 。

  • setAsynchronous : 异步消息。因为主线程的消息机制中已经添加了同步屏障,所以Handler只会处理异步消息。



紧接着上面调用的scheduleFrameLocked()方法 :

//Choreographer 

    private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSYNC) {//4.1及以上默认使用VSYNC垂直同步
                // If running on the Looper thread, then schedule the vsync immediately,
                // otherwise post a message to schedule the vsync from the UI thread
                // as soon as possible.
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();// UI 线程
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    //将异步消息放置Handler队列的最前面,当前是最高优先级。
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } 
            ``````
        }
    }


    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }

    //只能在ui线程中使用
    private final FrameDisplayEventReceiver mDisplayEventReceiver;
//DisplayEventReceiver

    /**
     * Schedules a single vertical sync pulse to be delivered when the next
     * display frame begins.
     */
    public void scheduleVsync() {
        //注册一个垂直同步脉冲VSYNC,当下一个脉冲到来时会回调dispatchVsync方法
        nativeScheduleVsync(mReceiverPtr);
    }

    private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
        onVsync(timestampNanos, builtInDisplayId, frame);
    }

从上面代码可以看出,Choreographer 即将要执行 垂直同步VSYNC信号了,如果当前在主线程,则立即调用scheduleVsyncLocked()方法,不是主线程则通过Handler(mainLooper)切换到 UI 线程,并且该Message是异步消息且放置消息队列的第一个,是最高优先级要处理的事情。


FrameDisplayEventReceiver

FrameDisplayEventReceiver 继承 DisplayEventReceiver , 主要是用来接收同步脉冲信号 VSYNC。

scheduleVsync()方法通过底层nativeScheduleVsync()向SurfaceFlinger 服务注册,即在下一次脉冲接收后会调用 DisplayEventReceiver的dispatchVsync()方法。

这里类似于订阅者模式,但是每次调用nativeScheduleVsync()方法都有且只有一次dispatchVsync()方法回调。

那么,这个同步脉冲信号 VSYNC 到底有何作用呢。



VSYNC

VSYNC 的全称是 Vertical Synchronization ,即垂直同步。

由于人眼与大脑之间的协作一般情况无法感知超过60FPS的画面更新。如果所看到画面的帧率高于12帧的时候,就会认为是连贯的,达到24帧便是流畅的体验,这也就是胶片电影的播放速度(24FPS)

这里写图片描述

对于屏幕显示,游戏体验来说,如果能整体平稳的达到60FPS,画面每秒更新60次,也就是16.67ms刷新一次,绝大部分人视觉体验都会觉得非常流畅如丝般顺滑。



Android 亦如此,正常情况下屏幕刷新频率也是60FPS

//获取手机屏幕刷新频率

Display display = getWindowManager().getDefaultDisplay();
float refreshRate = display.getRefreshRate();
//Display

    public float getRefreshRate() {
        return mRefreshRate;
    }

    //Display 内部类
    public static final class Mode implements Parcelable {
        private final float mRefreshRate;

        public Mode(``````, float refreshRate) {
            mRefreshRate = refreshRate;
        }

在VirtualDisplayDevice 类中,对Mode进行赋值,并且refreshRateintfinal常量值,也就是60HZ

//VirtualDisplayDevice

        private static final float REFRESH_RATE = 60.0f;

每秒钟 60 帧的屏幕刷新频率,也就是 1000 / 60 ≈ 16.67ms 。



在没有 VSYNC 同步信号脉冲情况下 : (Jank 为同一帧在屏幕上出现 2 次以上)
这里写图片描述

  1. 时间从0开始,进入第一个16ms:Display显示第0帧,CPU,GPU处理第一帧。
  2. 时间进入第二个16ms:因为早在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。Display可以正常显示第1帧。但在当前的 16ms期间,CPU和GPU却并未及时绘制第2帧数据(注意前面的空白区),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。
  3. 时间进入第三个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank)。



所以从Android 4.1Jelly Bean开始,Project Buffer引入了VSYNC,系统在收到VSync pulse后,将马上开始下一帧的渲染。结果如下图所示:
这里写图片描述

所以关键点在于 CPU 在每个 VSYNC 信号到来时,必须开始着手处理下一帧,然后交由 GPU 处理,最后再由屏幕(display)显示出来,整个流水线作业的步调一致是保证显示系统流畅的基础。并且程序的大多数 UI 操作都要在 16.67ms内执行完成。

如果在主线程的某个操作耗时 24ms ,那么用户在 32ms 内看到的都将会是同一帧画面
这里写图片描述
所以在主线程中的耗时操作会影响 ui 的流畅度。

更多Project Butter 可以参考 Android Project Butter分析 , 我们下一篇文章将会分析 CPU GPU 以及屏幕的渲染机制。




回归正文,我们继续看DisplayEventReceiver,当它订阅了下一次VSYNC 信号后,VSYNC 信号到来时,就会回调 dispatchVsync()方法,也就是上文所说的时机成熟的时候

//DisplayEventReceiver

    /**
     * Schedules a single vertical sync pulse to be delivered when the next
     * display frame begins.
     */
    public void scheduleVsync() {
        //注册一个垂直同步脉冲VSYNC,当下一个脉冲到来时会回调dispatchVsync方法
        nativeScheduleVsync(mReceiverPtr);
    }

    private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
        onVsync(timestampNanos, builtInDisplayId, frame);
    }
//Choreographer


    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            ``````
            mTimestampNanos = timestampNanos;//信号到来的时间参数
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);//this 就是当前的 run 方法
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);//切换到主线程,即执行下面的 run 方法
        }

        @Override
        public void run() {
            doFrame(mTimestampNanos, mFrame);
        }
    }

接收到 VSYNC 信号订阅事件后,回调 onVsync()方法,因为 UI 事件的处理一定要在主线程,所以FrameDisplayEventReceiver 实现 Runnable 的 run() 方法,通过 Handler 发送异步消息,由 run() 执行 UI 事件。

//Choreographer

    void doFrame(long frameTimeNanos, int frame) {

        try {
            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);//scheduleTraversals 方法postCallback的标识

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        }
    }


    void doCallbacks(int callbackType, long frameTimeNanos) {
        ``````
        try {
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                c.run(frameTimeNanos);
            }
    }

到这里我们就可以回归到 ViewRootImpl 类了

//ViewRootImpl

    void scheduleTraversals() {
        if (!mTraversalScheduled) {//在下一帧中,只会执行一次 doTraversal 遍历操作 !
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, //doCallbacks中回调CALLBACK_TRAVERSAL标识
                    mTraversalRunnable, null);
            ``````
        }
    }

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();//遍历视图 measure,layout,draw
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

兜了一圈回到了ViewRootImpl,上面大致说明了我们当前的遍历操作,对下一帧的准备工作是,当我们ViewRootImpl遍历结束,将绘制结果交给屏幕以便显示。
如果迟迟交不出View的绘制结果,那么屏幕将会一直显示当前画面。


Triple Buffer

Android 4.1 以前一直沿用double-buffer 双缓冲技术,也就是两块显示 Buffer,back buffer用于CPU/GPU下一帧的绘制准备,另一块 frame buffer 则用于屏幕显示,当back buffer准备就绪后,它们才进行交换。

但是如果我们的准备时间太久,有可能因为 主线程耗时阻塞,xml 布局文件层次过多冗余臃肿,绘制操作不当(onDraw中频繁创建对象) ,导致back buffer 缓冲数据迟迟没有准备好,那么屏幕上就会一直显示 frame buffer ,造成卡顿视觉。

这里写图片描述

  1. 当CPU / GPC 准备B Buffer 内容时间过长,导致第一个VSYNC信号到来时不能交付 back Buffer ,那么屏幕上显示的还是之前的那块 PRE Buffer , 并且 B Buffer 内容准备完成后,还需要等待下一个 VSYNC 信号才能交付。
  2. 因为在第二个 VSYNC 信号到来时,两块 Buffer 都已经被占用(一块用来显示 ,一块被 B Buffer 准备工作持有),所以当下一次绘制内容也存在延迟的情况也会造成连锁卡顿。(同一帧画面显示 2 次及以上)

这里写图片描述

解决上面问题的办法就是引入第三块 Buffer , 在渲染 B 超时而且 Buffer A 又用于屏幕显示时,可以用第三块 Buffer 来进行C 的准备工作,这样便减少了后面的一次 Jank 发生。

系统大部分情况下都会使用两个Buffer 来完成显示,只有在某一帧的处理时间超过 2 次 VSYNC 信号周期才会使用第三块 Buffer。



总结

最后补上一张本文整个流程的大致时序图,下篇文章将会继续探索CPU / GPU 渲染相关知识。

这里写图片描述
查看大图


参考

Android Project Butter分析
android屏幕刷新显示机制

Android Choreographer 源码分析/

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值