Android的屏幕刷新详解

1、基本概念

Android屏幕的刷新包括3个重要的部分:CPU、GPU、屏幕

  • 1、CPU:负责View的测量、布局、绘制,将操作完成的数据提交给GPU。
  • 2、GPU:负责对CPU提交的数据进行渲染,将渲染完成的数据写入buffer中。
  • 3、屏幕:每隔16.6ms从buffer中取出并展示在屏幕上。屏幕从buffer中读取数据的频率是固定的,但是CPU/GPU将数据写入缓存却是随机的。
    既然屏幕读取频率是固定的,CPU、GPU写数据是随机的,那么当屏幕读取数据的时候,CPU、GPU还在写数据,会导致一部分数据被复写,buffer里面的数据会来自不同帧,就会出现画面的“撕裂”。
    那么有什么方法可以减少画面的“撕裂”呢?

2、双缓冲机制

为了解决画面“撕裂”问题的,Android引入了双缓冲机制。从原先的单缓存变成了Frame Buffer和Back Buffer。GPU只负责向Back Buffer中写入数据,屏幕只从Frame Buffer中取数据,GPU会定期交换Back Buffer和Frame Buffer,交换的频率是60次/秒,这也就和屏幕的刷新频率保持一致。

3、丢帧是怎么发生的

3.1、布局嵌套过深、过多的View需要刷新或CPU性能较差

引入了双缓存就一定没有问题了吗?
当布局比较复杂或者CPU性能较差时,我们并不能保证绘制能在16.6ms内完成,这就有可能会导致到了Back Buffer和Frame Buffer交换的时间点时,GPU还在向Back Buffer中写数据,如果强行交换,那么这一帧的数据肯定是不完整的。
为此系统做了如下处理:
在GPU向Back Buffer中写数据时将Back Buffer锁住,不交换Back Buffer和Frame Buffer,让屏幕依然显示上一帧的内容,在下次交换的时间点到来时再进行交换。

这样就会导致丢帧。所以我们在开发中尽量较少布局的层级嵌套,减少不必要的View的刷新,避免过多对象的创建。

3.2、如果View的绘制保证在16.6ms内完成,就一定能避免丢帧吗?

即使View绘制耗时较少,但是如果在下次交换的时间点马上到来的时候,才开始绘制,这也会导致在交换时Back Buffer处于锁定状态,导致丢帧。

image.png

Vsync是屏幕刷新的信号,每隔16.6ms由底层发出。
如果能保证每次屏幕刷新信号到来的时候,就开始绘制,是不是就能解决这个问题呢?
image.png

Android系统开始引入Choreographer这个类来保证Vsync和绘制的同步。
从上图中可以看出:蓝色(屏幕)区域中的数字0、1、2、3、4就代表屏幕上一帧帧的数据,它是和绘制部分(绿色区域)中的数字对应的。每次Vsync信号到来的时候绘制的是下一帧的数据,绘制完成后并不会立马显示在屏幕上,只是将数据写入到Back Buffer中,等下一次Vsync信号到来时,GPU交换Back Buffer和Frame Buffer将之前绘制的数据显示在屏幕上,与此同时开始绘制下一帧的数据。

4、Choreographer

源码分析的难点就是在代码的海洋中找不到切入点,既然本文是在讲Android屏幕的刷新,那咱们就从View的invalidate()开始。View的invalidate()具体源码这里不做分析,只大致说下流程:

  • 1、View.invalidate()会调用其父类ViewGroup的invalidateChild()
  • 2、ViewGroup.invalidateChild()中循环查找parent,并调用parent.invalidateChildInParent()。
  • 3、最终会执行View树顶层的DecorView的parent ViewRootImpl.invalidateChildInParent()
 @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        //省略....
        invalidateRectOnScreen(dirty);
        return null;
    }

    private void invalidateRectOnScreen(Rect dirty) {
       //省略...
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }
    }

方法最终执行到了ViewRootImpl.scheduleTraversals();

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

终于到了关键的代码了,先说下这段代码的作用:向Choreographer发送一个信号告诉它我要刷新,然后等待下一个Vsync信号到来的时候去刷新View。
这部分有几个重点:

  • 1、mTraversalScheduled默认为false,进入判断后被置成true,那这个方法是不是就只能执行一次啊,肯定不是,下面我们再进行分析。
  • 2、mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();添加同步消息屏障,稍后分析。
  • 3、mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);向Choreographer发送消息。

4.1、Choreographer.postCallback()

 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) {
        if (action == null) {
            throw new IllegalArgumentException("action must not be null");
        }
        if (callbackType < 0 || callbackType > CALLBACK_LAST) {
            throw new IllegalArgumentException("callbackType is invalid");
        }

        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }

    private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        if (DEBUG_FRAMES) {
            Log.d(TAG, "PostCallback: type=" + callbackType
                    + ", action=" + action + ", token=" + token
                    + ", delayMillis=" + delayMillis);
        }

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            //以当前时间戳放进callbackType=Choreographer.CALLBACK_TRAVERSAL的mCallbackQueues队列中
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
            //由于postCallbackDelayed()传入的delayMillis=0,则dueTime = now
            if (dueTime <= now) {
                //如果没有消息延时,则直接执行
                scheduleFrameLocked(now);
            } else {
                //消息延时,最终依然会执行到scheduleFrameLocked()
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                //设置Message为异步消息
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

Choreographer##scheduleFrameLocked()

private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSYNC) {
            
                //判断是否在主线程
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                //非主线程中,发消息到主线程最终会执行scheduleVsyncLocked()
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    //设置消息为异步消息
                    msg.setAsynchronous(true);
                  //插到消息队列头部,可以理解为设置最高优先级,保证该消息能尽快执行
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
                //省略....
            }
        }
    }

Choreographer##scheduleVsyncLocked()
Choreographer.scheduleVsyncLocked()最终会调用DisplayEventReceiver.scheduleVsync()

/**
     * Schedules a single vertical sync pulse to be delivered when the next
     * display frame begins.
     */
    public void scheduleVsync() {
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                    + "receiver has already been disposed.");
        } else {
            nativeScheduleVsync(mReceiverPtr);
        }
    }

跟到这里调用native方法,跟不下去了,哈哈。既然这样,我们换种思路,还记得我们在ViewRootImpl.scheduleTraversals()向队列中存入了一个action等待执行,那么我们就找下这个action是在哪里取出来的。
Choreographer的内部类CallbackQueue

private final class CallbackQueue {
        private CallbackRecord mHead;

        public boolean hasDueCallbacksLocked(long now) {
            return mHead != null && mHead.dueTime <= now;
        }
        //取出操作
        public CallbackRecord extractDueCallbacksLocked(long now) {
            CallbackRecord callbacks = mHead;
            if (callbacks == null || callbacks.dueTime > now) {
                return null;
            }

            CallbackRecord last = callbacks;
            CallbackRecord next = last.next;
            while (next != null) {
                if (next.dueTime > now) {
                    last.next = null;
                    break;
                }
                last = next;
                next = next.next;
            }
            mHead = next;
            return callbacks;
        }

        //省略...    
    }

跟踪发现CallbackQueue.extractDueCallbacksLocked()Choreographer##doCallbacks()中调用
Choreographer##doCallbacks()

void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            // We use "now" to determine when callbacks become due because it's possible
            // for earlier processing phases in a frame to post callbacks that should run
            // in a following phase, such as an input event that causes an animation to start.
            final long now = System.nanoTime();
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }
            //省略...
    }

Choreographer##doCallbacks()Choreographer##doFrame()中调用

void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            //省略....

        try {
           //省略...
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        //省略....
    }

跟踪发现Choreographer##doFrame()被调用地方有几个,我们主要看下FrameDisplayEventReceiver类中的调用。

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
            super(looper, vsyncSource);
        }

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            //省略...
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            //组织Message时将callBack设置为this,在从MessageQueue中取出消息时会优先执行Message中的callBack,否则去执行Handler中的callBack,如果都未设置callBack则去执行handleMessage()
            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);
        }
    }

FrameDisplayEventReceiver类是继承DisplayEventReceiver接收底层Vsync信号开始处理UI的过程。Vsync是由SurfaceFlinger每隔16.6ms发送一次,当Vsync信号到来时会回调FrameDisplayEventReceiver的onVsync()方法发送消息到主线程,去执行run()中的doFrame()。

也就是说onVsync()方法是每隔16.6ms收到Vsync信号时回调的,那么问题来了:我们怎么知道Vsync信号来了?肯定是我们在某处注册了一个监听。还记得之前那个native方法吗?

public void scheduleVsync() {
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                    + "receiver has already been disposed.");
        } else {
            nativeScheduleVsync(mReceiverPtr);
        }
    }

这里我们就可以大胆猜测一下:这个native方法内部应该就是实现了注册监听的功能。这样才能做到SurfaceFlinger定时发送来Vsync信号时,回调onVsync()方法,然后发消息到主线程去执行doFrame()方法从mCallbackQueues中取出callbackType为Choreographer.CALLBACK_TRAVERSAL的队列,以时间戳从队列中取出先前调用ViewRootImpl.scheduleTraversals()中存入的Runnable去执行,最终会调用ViewRootImpl.doTraversal()
ViewRootImpl##doTraversal()

void doTraversal() {
    //mTraversalScheduled 置成false
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
//移除同步消息屏障            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

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

ViewRootImpl##performTraversals()

private void performTraversals() {

    if (...) {
         performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
        performLayout(lp, mWidth, mHeight);
    }

    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    if (!cancelDraw && !newSurface) {
        performDraw();
    } 
}

performTraversals()中就是对这个View树的测量、布局、绘制。
整个过程还是比较复杂了,这里做一下梳理:

  • 1、调用View.invalidate()刷新布局时会调用ViewRootImpl.scheduleTraversals()通过Choreographer的postCallBack()将View树的测量、布局、绘制操作封装到Runnable中,并以当前时间戳将其存入mCallbackQueues队列中(这个队列类似于MessageQueue,以时间戳进行排序),等待执行。
  • 2、在Choreographer中注册一个信号的监听,当SurfaceFlinger每隔16.6ms发送Vsync信号时就回调FrameDisplayEventReceiver的onVsync()方法,在onVsync()中发送一条异步消息到主线程中去执行doFrame()
  • 3、doFrame()中取出队列中先前存入的View树的测量、布局、绘制操作封装的Runnable去执行。
  • 4、所以说,当我们调用invalidate、requestLayout等刷新操作时,并不是马上会执行刷新的操作,而是通过ViewRootImpl 的 scheduleTraversals() 先向底层注册一个屏幕刷新的监听,然后等下一个屏幕刷新信号到来的时候才会调用performTraversals() 遍历绘制 View 树来执行这些刷新操作

5、过滤同一帧内多次刷新操作

问:如果同一帧内(16.6ms内),我们多次调用了invalidate或requestLayout,是不是就在底层注册了多个监听,当下次屏幕刷新信号到来的时候,然后执行多次perforTraversals(),多次遍历View树进行View的绘制呢?
答:为啥要多次遍历View树,即使一帧内有n个View需要刷新,我们也只需要遍历一次View树(毕竟这些View都在View树内)就可以完成n个View的刷新操作。google工程师也是这么做的,下面看下代码。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //....
        }
    }

mTraversalScheduled默认false,在第一个View刷新请求时,会进入判断在底层注册一个监听并把mTraversalScheduled置成true,那么后面再次过来的刷新请求就不再执行,那mTraversalScheduled又是在哪里被重置的呢。

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //.....
            }
        }
    }

doTraversal()中就将mTraversalScheduled重置成了false,也就是说当我们调用了一次scheduleTraversals()之后,直到下一个屏幕刷新信号来的时候,doTraversal()被取出来执行。在这期间内重复调用scheduleTraversals()都会被过滤掉的。

6、同步屏障消息

MessageQueue.postSyncBarrier()

该方法的作用是发送同步屏障。消息的同步屏障可以理解为拦截同步消息的执行。熟悉Handler消息机制的同学都知道,在主线程中Looper循环调用MessageQueue.next()中取出Message去执行,只有当Message执行完成后才会取下一个。当调用MessageQueue.next()取消息时,发现队头是一个同步屏障的消息时,就会遍历队列只寻找设置了异步标志的消息,如果找到这个异步消息就取出来执行,否则就让next()方法进入阻塞状态。如果next()陷入了阻塞,那么后面的同步消息都被拦截住了,直到这个同步的屏障消息被移除,才会处理后面的同步消息。

添加同步屏障的消息就是为了让部分优先级较高的消息能更快的执行。主线程中不可能只处理屏幕刷新,如果在MessageQueue中处理屏幕刷新的Message前面有很多其他的Message,那么在屏幕刷新信号到来的时候,我们并不能很快执行到屏幕刷新的Message,在16.6ms过去了,下一帧的屏幕刷新信号到来了,我们还没执行完View的绘制,这依然会导致丢帧。
在View调用invalidate()请求刷新时,会调用ViewRootImpl.scheduleTraversals()MessageQueue中发送一个同步屏障消息,即使View树遍历的Message前很多其他的同步消息,当屏幕刷新信号到来时也会优先执行View树的遍历(doTraversal()中移除同步屏障消息,不再拦截其他同步消息),保证屏幕刷新信号和View树的遍历的同步。
那么,有了同步屏障消息的控制就能保证每次一接收到屏幕刷新信号就第一时间处理遍历绘制 View 树的工作么?

只能说,同步屏障是尽可能去做到,但并不能保证一定可以第一时间处理。因为,同步屏障是在 scheduleTraversals() 被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在 scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行。

总结

  • 1、由于屏幕从buffer中读取数据的频率是固定,但是CPU、GPU向buffer中写入数据却是随机了,这就有可能导致画面的撕裂,为此引入了双缓冲机制。
  • 2、如果在屏幕刷新信号到来时,CPU、GPU还在向Back Buffer中写数据,那么Back Buffer将被锁定,等待下次的屏幕刷新信号到来时GPU再切换Back Buffer为Frame Buffer,这也就导致了丢帧。
  • 3、即使View的绘制很快,如果不能保证屏幕刷新信号和绘制的同步,在下一帧屏幕刷新信号快来时才开始执行View的绘制也会导致BackBuffer的被锁定,最终导致丢帧。
  • 4、Choreographer就是为了尽可能保证屏幕刷新信号和View绘制的同步。在View调用invalidate或者requestLayout时就调用ViewRootImpl.scheduleTraversals()MessageQueue中发送一个同步屏障的消息,并注册一个信号监听,将View的绘制封装成Runnable存入队列中等待下一帧屏幕刷新信号到来时,onVsync()就会被回调发送异步消息到主线程(由于有同步屏障消息,异步消息优先执行),调用doFrame()取出之前存入Runnable去执行View的绘制。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348