【面试专题】Android屏幕刷新机制

这个问题在其他人整理的面试宝典中也有提及,一般来说都是问View的刷新,基本上从ViewRootImpl的scheduleTraversals()方法开始讲就可以了。之前看别人面试斗鱼的面经,被问到了Android屏幕刷新机制、双缓冲、三缓冲、黄油计划,然后我面网易云的时候也确实被问到了这个题目。

屏幕刷新这一整套,你把我这篇文章里的内容讲清楚了,肯定ok了。网易云还附加问了我CPU和GPU怎么交换绘制数据的,这个我个人认为完全是加分题了,我答不出来,感兴趣的小伙伴可以去看一看,你要是能说清楚,肯定能让面试官眼前一亮。

双缓冲

在讲双缓冲这个概念之前,先来了解一些基础知识。

显示系统基础

在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分, CPU负责计算帧数据,把计算好的数据交给GPU, GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数 据呈现到屏幕上。

  • 画面撕裂

屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一 帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即 buffer里的数据可能是来自不同的帧的。当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。

简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。

那咋解决画面撕裂呢? 答案是使用双缓冲。

双缓冲

由于图像绘制和屏幕读取 使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。

双缓冲,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。

VSync

什么时候进行两个buffer的交换呢?

假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。 看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。

当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现画面撕裂的状况。

VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。

所以说VSync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了。

Android屏幕刷新机制

先总体概括一下,Android屏幕刷新使用的是“双缓存+VSync机制”,单纯的双缓冲模式容易造成jank(丢帧)现象,为了解决这个问题,Google在 Android4.1 提出了Project Butter(⻩油工程),引入了 drawing with VSync 的概念。

jank(丢帧)

VSync.jpeg

以时间的顺序来看下将会发生的过程:

  1. Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,且在Display显示下一帧前完成
  2. 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧
  3. 接着第2帧开始处理,是直到第2个VSync快来前才开始处理的。
  4. 第2个VSync来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为“Jank”,即发生了丢帧。
  5. 当第2帧数据准备完成后,它并不会⻢上被显示,而是要等待下一个VSync 进行缓存交换再显示。

所以总的来说,就是屏幕平白无故地多显示了一次第1帧。 原因是第2帧的CPU/GPU计算 没能在VSync信号到来前完成。

这里注意一下一个细节,jank(丢帧、掉帧),不是说这一帧丢弃了不显示,而是这一帧延迟显示了,因为缓存交换的时机只能等下一个VSync了。

黄油计划 —— drawing with VSync

为了优化显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,实现了Project Butter(⻩油工程): 系统在收到VSync pulse后,将⻢上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU 才立刻开 始计算然后把数据写入buffer。如下图:

VSync2.jpeg

CPU/GPU根据VSYNC信号同步处理数据,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。 一句话总结,VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank。

问题又来了,如果界面比较复杂,CPU/GPU的处理时间较⻓,超过了16.6ms呢?如下图:

VSync3.jpeg
  1. 在第二个时间段内,但却因 GPU 还在处理 B 帧,缓存没能交换,导致 A 帧被重复显示。
  2. 而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个signal的来临。于是在这一过程中,有一大段时间是被浪费的。
  3. 当下一个VSync出现时,CPU/GPU⻢上执行操作(A帧),且缓存交换,相应的显示屏对应的就是B。这时看起来就是正常的。只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。

为什么 CPU 不能在第二个 16ms 处理绘制工作呢? 因为只有两个 buffer,Back buffer正在被GPU用来处理B帧的数据, Frame buffer的内容用于Display的显示,这样两个 buffer都被占用,CPU 则无法准备下一帧的数据。 那么,如果再提供一个buffer,CPU、GPU 和显示设备都能使用各自的 buffer工作,互不影响。这就是三缓冲的来源了。

三缓冲

三缓存就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。

VSync4.jpeg
  1. 第一个Jank,是不可避免的。但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是 会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。
  2. 注意在第3段中,A帧的计算已完成,但是在第4个vsync来的时候才显示,如果是双缓冲,那在第三个vynsc就可以显示了。

三缓冲有效利用了等待VSync的时间,减少了jank,但是带来了延迟。是不是 Buffer 越多越好呢?这个是否定的, Buffer 正常还是两个,当出现 Jank 后三个足以。

Choreographer

上边讲的都是基础的刷新知识,那么在 Android 系统中,真正来实现绘制的类叫Choreographer

Choreographer负责对CPU/GPU绘制的指导 —— 收到VSync信号才开始绘制,保证绘制拥有完整 16.6ms,避免绘制的随机性。

通常 应用层不会直接使用Choreographer,而是使用更高级的API,例如动画和View绘制相关的 ValueAnimator.start()、View.invalidate()等。

(这边补充说一个面试题,属性动画更新时会回调onDraw吗?不会,因为它内部是通过AnimationHandler中的Choreographer机制来实现的更新,具体的逻辑,如果以后有时间的话可以写篇文章来说一说。)

业界一般通过Choreographer来监控应用的帧率。

(这个东西也是个面试题,会问你如何检测应用的帧率?你可以提一下Choreographer里面的FrameCallback,然后结合一些第三方库的实现具体说一下。)

View刷新的入口

Activity启动,走完onResume方法后,会进行window的添加。window添加过程会调用ViewRootImpl的setView()方法, setView()方法会调用requestLayout()方法来请求绘制布局,requestLayout()方法内部又会走到scheduleTraversals()方法。最后会走到performTraversals()方法,接着到了我们熟知的测量、布局、绘制三大流程了。

当我们使用 ValueAnimator.start()、View.invalidate()时,最后也是走到ViewRootImpl的 scheduleTraversals()方法。(View.invalidate()内部会循环获取ViewParent直到ViewRootImpl的invalidateChildInParent()方法,然后走到scheduleTraversals(),可自行查看源码)

即所有UI的变化都是走到ViewRootImpl的scheduleTraversals()方法。

这里注意一个点:scheduleTraversals()之后不是立即就执行performTraversals()的,它们中间隔了一个Choreographer机制。简单来说就是scheduleTraversals()中,Choreographer会去请求native的VSync信号,VSync信号来了之后才会去调用performTraversals()方法进行View绘制的三大流程。


 //ViewRootImpl.java
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //添加同步屏障,屏蔽同步消息,保证VSync到来立即执行绘制
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); 
        //mTraversalRunnable是TraversalRunnable实例,最终走到run(),也即doTraversal();
        mChoreographer.postCallback(
                      Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
         doTraversal();
    } 
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步屏障 
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ...
        //开始三大绘制流程
        performTraversals();
        ...
    } 
}
复制代码
  1. postSyncBarrier 开启同步屏障,保证VSync到来后立即执行绘制
  2. mChoreographer.postCallback()方法,发送一个会在下一帧执行的回调,即在下一个VSync到来时会执行 TraversalRunnable–>doTraversal()—>performTraversals()–>绘制流程。

Choreographer

初始化

mChoreographer,是在ViewRootImpl的构造方法内使用 Choreographer.getInstance()创建。

Choreographer和Looper一样是线程单例的,通过ThreadLocal机制来保证唯一性。因为Choreographer内部通过FrameHandler来发送消息,所以初始化的时候会先判断当前线程有无Looper,没有的话直接抛异常。

public static Choreographer getInstance() {
    return sThreadInstance.get();
}

private static final ThreadLocal sThreadInstance =
              new ThreadLocal() {
    @Override
    protected Choreographer initialValue() {
         Looper looper = Looper.myLooper();
         if (looper == null) {
         //当前线程要有looper,Choreographer实例需要传入
              throw new IllegalStateException("The current thread must have a looper!");
        }
        Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
        if (looper == Looper.getMainLooper()) {
            mMainInstance = choreographer;
        }
        return choreographer;
   }
};
复制代码

postCallback

mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)方法,第一个参数是CALLBACK_TRAVERSAL,表示回调任务的类型,共有以下5种类型:

//输入事件,首先执行
public static final int CALLBACK_INPUT = 0; 
//动画,第二执行
public static final int CALLBACK_ANIMATION = 1; 
//插入更新的动画,第三执行
public static final int CALLBACK_INSETS_ANIMATION = 2; 
//绘制,第四执行
public static final int CALLBACK_TRAVERSAL = 3; 
//提交,最后执行,
public static final int CALLBACK_COMMIT = 4;
复制代码

五种类型任务对应存入对应的CallbackQueue中,每当收到 VSYNC 信号时,Choreographer 将首先处理 INPUT 类型的任 务,然后是 ANIMATION 类型,最后才是 TRAVERSAL 类型。

postCallback()内部调用postCallbackDelayed(),接着又调用postCallbackDelayedInternal(),正常消息执行scheduleFrameLocked,延迟运行的消息会发送一个MSG_DO_SCHEDULE_CALLBACK类型的meessage:

private void postCallbackDelayedInternal(int callbackType,
      Object action, Object token, long delayMillis) {
    ...
    synchronized (mLock) {
        ...
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
        if (dueTime <= now) { //立即执行
             scheduleFrameLocked(now);
        } else {
            //延迟运行,最终也会走到scheduleFrameLocked()
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); 
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        } 
    }
}
复制代码

FrameHandler这个类是内部专门用来处理消息的,可以看到延迟的MSG_DO_SCHEDULE_CALLBACK类型消息最终也是走到scheduleFrameLocked:

private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
        super(looper);
    }
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_DO_FRAME:
                // 执行doFrame,即绘制过程 
                doFrame(System.nanoTime(), 0);
                break;
            case MSG_DO_SCHEDULE_VSYNC: 
                //申请VSYNC信号,例如当前需要绘制任务时 
                doScheduleVsync();
                break;
            case MSG_DO_SCHEDULE_CALLBACK: 
                //需要延迟的任务,最终还是执行上述两个事件 
                doScheduleCallback(msg.arg1);
                break;
        } 
    }
}

void doScheduleCallback(int callbackType) {
    synchronized (mLock) {
        if (!mFrameScheduled) {
            final long now = SystemClock.uptimeMillis();
            if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
                scheduleFrameLocked(now);
            }
        } 
    }
}
复制代码

申请VSync信号

scheduleFrameLocked()方法里面就会去真正的申请 VSync 信号了。

private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true; 
        if (USE_VSYNC) {
            //当前执行的线程,是否是mLooper所在线程
            if (isRunningOnLooperThreadLocked()) {
                //申请 VSYNC 信号
                scheduleVsyncLocked();
            } else {
                // 若不在,就用mHandler发送消息到原线程,最后还是调用scheduleVsyncLocked方法 
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); 
                msg.setAsynchronous(true);//异步 
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } else {
            // 如果未开启VSYNC则直接doFrame方法(4.1后默认开启) 
            final long nextFrameTime = Math.max(
            mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
            Message msg = mHandler.obtainMessage(MSG_DO_FRAME); 
            msg.setAsynchronous(true);//异步 
            mHandler.sendMessageAtTime(msg, nextFrameTime);
        } 
    }
}
复制代码

VSync信号的注册和监听是通过mDisplayEventReceiver实现的。mDisplayEventReceiver是在Choreographer的构造方法中创建的,是FrameDisplayEventReceiver的实例。 FrameDisplayEventReceiver是 DisplayEventReceiver 的子类,

private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}
复制代码
public DisplayEventReceiver(Looper looper, int vsyncSource) {
    if (looper == null) {
        throw new IllegalArgumentException("looper must not be null");
    }
    mMessageQueue = looper.getQueue();
    // 注册native的VSYNC信号监听者
    mReceiverPtr = nativeInit(new WeakReference(this), mMessageQueue,vsyncSource);
    mCloseGuard.open("dispose");
}
复制代码

VSync信号回调

native的VSync信号到来时,会走到onVsync()回调:

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {

    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
        ...
        //将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息 
        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);
    }
}
复制代码

(这里补充一个面试题:页面UI没有刷新的时候onVsync()回调也会执行吗?不会,因为VSync是UI需要刷新的时候主动去申请的,而不是native层不停地往上面去推这个回调的,这边要注意。)

doFrame

doFrame()方法中会通过doCallbacks()方法去执行各种callbacks,主要内容就是取对应任务类型的队列,遍历队列执行所有任务,其中就包括了 ViewRootImpl 发起的绘制任务mTraversalRunnable了。mTraversalRunnable执行doTraversal()方法,移除同步屏障,调用performTraversals()开始三大绘制流程。

到这里整个流程就闭环了。

本文在开源项目:https://github.com/Android-Alvin/Android-LearningNotes 中已收录,里面包含了Android组件化最全开源项目(美团App、得到App、支付宝App、微信App、蘑菇街App、有赞APP...)等,资源持续更新中...

你可能感兴趣的:(【面试专题】Android屏幕刷新机制)