Android 屏幕刷新机制详解

一、屏幕刷新机制概述

在一个典型的显示系统中,一般包括CPU、GPU、display三个部分, CPU负责计算数据,把计算好数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer里存起来,然后display负责把buffer里的数据呈现到屏幕上。很多时候,我们可以把CPU、GPU放在一起说,那么就是包括2部分,CPU/GPU 和display。

为什么会产生tearing?

显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,display从buffer中取出数据,然后一行一行显示出来。display处理的频率是固定的,比如每隔60ms显示完一帧,但是CPU/GPU写数据是不可控的,所以会出现有些数据根本没显示出来就被重写了,buffer里的数据可能是来自不同的帧的, 所以出现画面“割裂”。

怎么解决tearing问题?

可以使用双缓存来解决tearing问题,基本原理就是采用两块buffer。一块back buffer用于CPU/GPU后台绘制,另一块framebuffer则用于显示,当back buffer准备就绪后,它们才进行交换。不可否认,double buffering可以在很大程度上降低screen tearing错误。

double buffering存在什么问题?

在这里插入图片描述

以时间的顺序来看下将会发生的异常:

  • Step1. Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,而且赶在Display显示下一帧前完成
  • Step2. 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,正常显示第1帧
  • Step3. 由于某些原因,比如CPU资源被占用,系统没有及时地开始处理第2帧,直到第2个VSync快来前才开始处理
  • Step4. 第2个VSync来时,由于第2帧数据还没有准备就绪,显示的还是第1帧。这种情况被Android开发组命名为“Jank”。
  • Step5. 当第2帧数据准备完成后,它并不会马上被显示,而是要等待下一个VSync。

所以总的来说,就是屏幕平白无故地多显示了一次第1帧。原因大家应该都看到了,就是CPU没有及时地开始着手处理第2帧的渲染工作,以致“延误军机”。 Android在4.1之前一直存在这个问题。

Android系统是如何解决双缓存存在的问题的?

为了优化显示性能,android 4.1版本对Android Display系统进行了重构,实现了Project Butter,引入了三个核心元素,即VSYNC、Triple Buffer和Choreographer。

二、UI渲染流程

1. scheduleTraversals()

界面上任何一个 View 的刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里来安排一次遍历绘制 View 树的任务。

scheduleTraversals() 会先过mTraversalScheduled滤掉同一帧内的重复调用,确保同一帧内只需要安排一次遍历绘制 View 树的任务,遍历过程中会将所有需要刷新的 View 进行重绘。

scheduleTraversals() 会往主线程的消息队列中发送一个同步屏障,拦截这个时刻之后所有的同步消息的执行,但不会拦截异步消息,以此来尽可能的保证当接收到屏幕刷新信号时可以尽可能第一时间处理遍历绘制 View 树的工作。

发完同步屏障后 scheduleTraversals() 将 performTraversals() 封装到 Runnable 里面,然后调用 Choreographer 的 postCallback() 方法。

简述:View的刷新都会从ViewRootImpl中的 scheduleTraversals() ,这个方法里边首先会发送一个同步屏障,阻塞同步消息,接下来通过mChoreographer post出一个Runnable

代码如下:

 // ViewRootImpl
 @UnsupportedAppUsage
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

2. Choreographer与Vsync

postCallback() 方法会先将这个 Runnable 任务以当前时间戳放进一个待执行的队列里,然后会调用一个native 层方法,这个native方法是用来向底层订阅下一个屏幕刷新信号Vsync,当下一个屏幕刷新信号发出时,底层就会通过 FrameDisplayEventReceiver 的onVsync() 方法来通知上层 app。onVsync() 方法被回调时,会往主线程的消息队列中发送一个执行 doFrame() 方法的异步消息。doFrame() 方法会去取出之前放进待执行队列里的任务来执行,取出来的这个任务实际上是 ViewRootImpl 的 doTraversal() 操作。

简述:mChoreographer中会将Runable放入执行队列,然后等待接受Vsync的信号,信号到来时通过FrameDisplayEventReceiver调用这个Runable,并最终执行ViewRootImpl中的doTraversal方法

// Choreographer
public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 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;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
            super(looper, vsyncSource);
        }
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {

            long now = System.nanoTime();
            if (timestampNanos > now) {
                timestampNanos = now;
            }

            if (mHavePendingVsync) {
            } else {
                mHavePendingVsync = true;
            }
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

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

3. 开启绘制流程

doTraversal()中首先移除同步屏障,再会调用performTraversals() 方法根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程,在这几个流程中都会去遍历 View 树来刷新需要更新的View。等到下一个Vsync信号到达,将上面计算好的数据渲染到屏幕上,同时如果有必要开始下一帧的数据处理。

简述:doTraversal()中首先移除同步屏障,然后调用performTraversals()方法根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程

// ViewRootImpl
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
                // 开启View的绘制流程
        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

Android 屏幕刷新机制

“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!

https://blog.csdn.net/chenzhiqin20/article/details/8628952

三、相关面试题

1.丢帧一般是什么原因引起的?

1)布局过于复杂或者存在大量OverDraw,致使解析绘制流程事件过长,CPU/GPU不能在一个刷新周期内完成数据的计算和绘制造成丢帧。

2)主线程有耗时操作,耽误了View的绘制。

2.Android刷新频率60帧/秒,每隔16ms调onDraw绘制一次?

显示器每隔16ms会刷新一次,但是只有用户发起重绘请求才会调用onDraw。

3.onDraw完之后屏幕会马上刷新么?

不会,会等待下一个Vsync信号。

4.如果界面没有重绘,还会每隔16ms刷新屏幕么?

对于底层显示器,每间隔16.6ms接收到VSYNC信号时,就会用buffer中数据进行一次显示。所以一定会刷新。(用的旧的数据)

5.如果在屏幕快刷新的时候才去onDraw绘制会丢帧么

代码发起的View的重绘不会马上执行,会等待下次VSYNC信号来的时候才开始。什么时候绘制没影响。

6.如果快速调用10次requestLayout,会调用10次onDraw吗?

mTraversalScheduled这个变量是为了过滤一帧内重复的刷新请求,初始值是false,在开始这一帧的绘制流程时候也会重新置为false(doTraversal()中,一会儿分析),同时,在取消遍历绘制 View 操作 unscheduleTraversals() 里也会设置为false。也就是说一般情况下在开始这一帧的正式绘制前,在这期间重复调用scheduleTraversals()只有一次会生效。这么设计的原因是前面已经说了和ViewRootImpl绑定的是DecorView,当刷新时候会对整个DecorView进行一次处理,所以不同view触发的scheduleTraversals()作用都是一样的,所以在这一帧里面只要有一次和多次刷新请求效果是一样的。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true; //防止多次调用
            // 发送同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
        }
    }

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            // 移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            ...
            performTraversals();
            ...
        }
    }

7.View 刷新机制

当我们调用 View 的 invalidate 时刷新视图时,它会调到 ViewRootImp 的 invalidateChildInParent,这个方法首先会 checkThread 检查是否是主线程,然后调用其 scheduleTraversals 方法。这个方法就是视图绘制的开始,但是它并不是立即去执行 View 的三大流程,而是先往消息队列里面添加一个同步屏障,然后在往 Choreographer 里面注册一个 TRAVERSAL 的回调。在下一次 Vsync 信号到来时,会去执行 doTraversals 方法。

Choreographer 主要是用来接收 Vsync 信号,并且在信号到来时去处理一些回调事件。事件类型有四种,分别是 Input、Animation、Traversal、Commit。在 Vsync 信号到来时,会依次处理这些事件,前三种比较好理解,第四种 Commit 是用来执行组件的 onTrimMemory 函数的。Choreographer 是通过 FrameDisplayEventReceiver 来监听底层发出的 Vsync 信号的,然后在它的回调函数 onVsync 中去处理,首先会计算掉帧,然后就是 doCallbacks 处理上面所说的回调事件。

Vsync 信号可以理解为底层硬件的一个消息脉冲,它每 16ms 发出一次,它有两种方式发出,一种是 HWComposer 硬件产生,一种是用软件模拟,即 VsyncThread。不管使用哪种方式,都统一由 DispSyncThread 进行分发。

教程来源于Github,感谢zhpanvip大佬的无私奉献,致敬!

技术教程推荐

赵成的运维体系管理课 -〔赵成〕

Java核心技术面试精讲 -〔杨晓峰〕

Go语言核心36讲 -〔郝林〕

如何设计一个秒杀系统 -〔许令波〕

黄勇的OKR实战笔记 -〔黄勇〕

高并发系统设计40问 -〔唐扬〕

Netty源码剖析与实战 -〔傅健〕

Vim 实用技巧必知必会 -〔吴咏炜〕

Python实战 · 从0到1搭建直播视频平台 -〔Barry〕