美文网首页
【面试专题】Android屏幕刷新机制

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

作者: Kepler_II | 来源:发表于2021-06-09 21:16 被阅读0次

    这个问题在其他人整理的面试宝典中也有提及,一般来说都是问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<Choreographer> sThreadInstance =
                  new ThreadLocal<Choreographer>() {
        @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<DisplayEventReceiver>(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屏幕刷新机制

          本文链接:https://www.haomeiwen.com/subject/mokyeltx.html