Android屏幕刷新机制解析(转)

android屏幕刷新显示机制

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

显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,屏幕每隔一段时间去buffer里取数据,然后显示出来。屏幕读取的频率是固定的,但是CPU/GPU写数据是完全无规律的。

上述内容概括一下,大体意思就是说,屏幕的刷新包括三个步骤:CPU 计算屏幕数据、GPU 进一步处理和缓存、最后 display 再将缓存中(buffer)的屏幕数据显示出来

对于 Android 而言,第一个步骤:CPU 计算屏幕数据指的也就是 View 树的绘制过程,也就是 Activity 对应的视图树从根布局 DecorView 开始层层遍历每个 View,分别执行测量、布局、绘制三个操作的过程。

也就是说,我们常说的 Android 每隔 16.6ms 刷新一次屏幕其实是指:底层以固定的频率,比如每 16.6ms 将 buffer 里的屏幕数据显示出来

如果还不清楚,那再看一张网上很常见的图:

image

Display 这一行可以理解成屏幕,所以可以看到,底层是以固定的频率发出 VSync 信号的,而这个固定频率就是我们常说的每 16.6ms 发送一个 VSync 信号。Display 黄色的这一行里有一些数字:0, 1, 2, 3, 4,可以看到每次屏幕刷新信号到了的时候,数字就会变化,所以这些数字其实可以理解成每一帧屏幕显示的画面。也就是说,屏幕每一帧的画面可以持续 16.6ms,当过了 16.6ms,底层就会发出一个屏幕刷新信号,而屏幕就会去显示下一帧的画面。

接下去就还是看这图,然后讲讲我们 app 层该干的事了:

继续看图,CPU 蓝色这段时间就跟我们自己写的代码有关系了,如果你的布局很复杂,层次嵌套很多,每一帧内需要刷新的 View 又很多时,那么每一帧的绘制耗时自然就会多一点。

继续看图,CPU 蓝色这行里也有一些数字,其实这些数字跟 Display 黄色的那一行里的数字是对应的,在 Display 里我们解释过这些数字表示的是每一帧的画面,那么在 CPU 这一行里,其实就是在计算对应帧的画面数据,也叫屏幕数据。也就是说,在当前帧内,CPU 是在计算下一帧的屏幕画面数据,当屏幕刷新信号到的时候,屏幕就去将 CPU 计算的屏幕画面数据显示出来;同时 CPU 也接收到屏幕刷新信号,所以也开始去计算下一帧的屏幕画面数据。

CPU 跟 Display 是不同的硬件,它们是可以并行工作的。要理解的一点是,我们写的代码,只是控制让 CPU 在接收到屏幕刷新信号的时候开始去计算下一帧的画面工作。而底层在每一次屏幕刷新信号来的时候都会去切换这一帧的画面,这点我们是控制不了的,是底层的工作机制。之所以要讲这点,是因为,当我们的 app 界面没有必要再刷新时,我们 app 是接收不到屏幕刷新信号的,所以也就不会让 CPU 去计算下一帧画面数据,但是底层仍然会以固定的频率来切换每一帧的画面,只是它后面切换的每一帧画面都一样,所以给我们的感觉就是屏幕没刷新。

那么我们就先稍微来梳理一下:

1、我们常说的 Android 每隔 16.6 ms 刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面。

2、这个每一帧的画面也就是我们的 app 绘制视图树(View 树)计算而来的,这个工作是交由 CPU 处理,耗时的长短取决于我们写的代码。

3、CPU 绘制视图树来计算下一帧画面数据的工作是在屏幕刷新信号来的时候才开始工作的,而当这个工作处理完毕后,也就是下一帧的画面数据已经全部计算完毕,也不会马上显示到屏幕上,而是会等下一个屏幕刷新信号来的时候再交由底层将计算完毕的屏幕画面数据显示出来。

4、当我们的 app 界面不需要刷新时(用户无操作,界面无动画),app 就接收不到屏幕刷新信号所以也就不会让 CPU 再去绘制视图树计算画面数据工作,但是底层仍然会每隔 16.6 ms 切换下一帧的画面,只是这个下一帧画面一直是相同的内容。

ViewRootImpl 与 DecorView 的绑定

View#invalidate() 是请求重绘的一个操作,我们跟着 invalidate() 一步步往下走的时候,发现最后跟到了 ViewRootImpl#scheduleTraversals() 就停止了。而 ViewRootImpl 就是今天我们要介绍的重点对象了。

Android 设备呈现到界面上的大多数情况下都是一个 Activity,真正承载视图的是一个 Window,每个 Window 都有一个 DecorView,我们调用 setContentView() 其实是将我们自己写的布局文件添加到以 DecorView 为根布局的一个 ViewGroup 里,构成一颗 View 树。但其实 DecorView 还有 mParent,而且就是 ViewRootImpl,而且每个界面上的 View 的刷新,绘制,点击事件的分发其实都是由 ViewRootImpl 作为发起者的,由 ViewRootImpl 控制这些操作从 DecorView 开始遍历 View 树去分发处理。

为什么 DecorView 的 mParent 会是 ViewRootImpl 呢?换个问法也就是,在什么时候将 DevorView 和 ViewRootImpl 绑定起来?

Activity 的启动是在 ActivityThread 里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate(), onStart(), onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个 addView() 最终其实是调用了 WindowManagerGlobal 的 addView() 方法,我们就从这里开始看:


//WindowManagerGlobal#addView

publicvoidaddView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow){

...

ViewRootImpl root;

...

synchronized(mLock) {

...

//1\. 实例化一个 ViewRootImpl对象

root =newViewRootImpl(view.getContext(), display);

...

mViews.add(view);

mRoots.add(root);

...

}

try{

//2\. 调用ViewRootImpl的setView(),并将DecorView作为参数传递进去

root.setView(view, wparams, panelParentView);

}...

}

WindowManager 维护着所有 Activity 的 DecorView 和 ViewRootImpl。这里初始化了一个 ViewRootImpl,然后调用了它的 setView() 方法,将 DevorView 作为参数传递了进去。所以看看 ViewRootImpl 中的 setView() 做了什么:


//ViewRootImpl#setView

publicvoidsetView(View view, WindowManager.LayoutParams attrs, View panelParentView){

synchronized(this) {

if(mView ==null) {

//1\. view 是 DecorView

mView = view;

...

//2.发起布局请求

requestLayout();

...

//3.将当前ViewRootImpl对象this,作为参数调用了DecorView的assignParent

view.assignParent(this);

...

}

}

}

在 setView() 方法里调用了 DecorView 的 assignParent() 方法,所以去看看 View 的这个方法:


//View#assignParent

voidassignParent(ViewParent parent){

if(mParent ==null) {

mParent =null;

}elseif(parent ==null) {

mParent =null;

}else{

thrownewRunTimeException("view "+this+" is already has a parent")

}

}

参数是 ViewParent,而 ViewRootImpl 是实现了 ViewParent 接口的,所以在这里就将 DecorView 和 ViewRootImpl 绑定起来了。每个Activity 的根布局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl。

跟界面刷新相关的方法里应该都会有一个循环找 parent 的方法,或者是不断调用 parent 的方法,这样最终才都会走到 ViewRootImpl 里,也就是说实际上 View 的刷新都是由 ViewRootImpl 来控制的。

即使是界面上一个小小的 View 发起了重绘请求时,都要层层走到 ViewRootImpl,由它来发起重绘请求,然后再由它来开始遍历 View 树,一直遍历到这个需要重绘的 View 再调用它的 onDraw() 方法进行绘制。

我们重新看回 ViewRootImpl 的 setView() 这个方法,这个方法里还调用了一个 requestLayout() 方法:


//ViewRootImpl#requestLayout

@Override

publicvoidrequestLayout(){

if(!mHandingLayoutInLayoutRequest) {

//1.检查该操作是否是在主线程中执行

checkThread();

mLayoutRequested =true;

//2.安排一次遍历绘制View树的任务

scheduleTraversals();

}

}

这里调用了一个 scheduleTraversals(),还记得当 View 发起重绘操作 invalidate() 时,最后也调用了 scheduleTraversals() 这个方法么。其实这个方法就是屏幕刷新的关键,它是安排一次绘制 View 树的任务等待执行。

也就是说,其实打开一个 Activity,当它的 onCreate---onResume 生命周期都走完后,才将它的 DecoView 与新建的一个 ViewRootImpl 对象绑定起来,同时开始安排一次遍历 View 任务也就是绘制 View 树的操作等待执行,然后将 DecoView 的 parent 设置成 ViewRootImpl 对象。

这也就是为什么在 onCreate---onResume 里获取不到 View 宽高的原因,因为在这个时刻 ViewRootImpl 甚至都还没创建,更不用说是否已经执行过测量操作了。

还可以得到一点信息是,一个 Activity 界面的绘制,其实是在 onResume() 之后才开始的。

ViewRootImpl#scheduleTraversals

到这里,我们梳理清楚了,调用一个 View 的 invalidate() 请求重绘操作,内部原来是要层层通知到 ViewRootImpl 的 scheduleTraversals() 里去。而且打开一个新的 Activity,它的界面绘制原来是在 onResume() 之后也层层通知到 ViewRootImpl 的 scheduleTraversals() 里去。虽然其他关于 View 的刷新操作,比如 requestLayout() 等等之类的方法我们还没有去看,但我们已经可以大胆猜测,这些跟 View 刷新有关的操作最终也都会层层走到 ViewRootImpl 中的 scheduleTraversals() 方法里去的。

那么这个方法究竟干了些什么,我们就要好好来分析了:


//ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
            Choreograhper.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

mTraversalScheduled 这个 boolean 变量的作用等会再来看,先看看 mChoreographer.postCallback() 这个方法,传入了三个参数,第二个参数是一个 Runnable 对象,先来看看这个 Runnable:

//ViewRootImpl$TraversalRunnable
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

//ViewRootImpl成员变量
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
这个 Runnable 做的事很简单,就调用了一个方法,doTraversal():

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

        //1. 遍历绘制View树
        performTraversals();
        ...
    }
}

看看这个方法做的事,跟 scheduleTraversals() 正好相反,一个将变量置成 true,这里置成 false,一个是 postSyncBarrier(),这里是 removeSyncBarrier(),具体作用等会再说,继续先看看 performTraversals(),这个方法也是屏幕刷新的关键:

//ViewRootImpl#performTraversals
private void performTraversals() {
    //该方法实在太过复杂,所以将无关代码全部都省略掉,只留下关键代码和代码结构
    ...
    if (...) {
        ...
        if (...) {
            if (...) {
                ...
                //1.测量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ...

                layoutRequested = true;
            }
        }
    } ...

    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    ...

    if (didLayout) {
        //2.布局
        performLayout(lp, mWidth, mHeight);
        ...
    }

    ...

    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

    if (!cancelDraw && !newSurface) {
        ...
        //3.绘制
        performDraw();
    }...

    ...
}

View 的测量、布局、绘制三大流程都是交由 ViewRootImpl 发起,而且还都是在 performTraversals() 方法中发起的,所以这个方法的逻辑很复杂,因为每次都需要根据相应状态判断是否需要三个流程都走,有时可能只需要执行 performDraw() 绘制流程,有时可能只执行 performMeasure() 测量和 performLayout() 布局流程(一般测量和布局流程是一起执行的)。不管哪个流程都会遍历一次 View 树,所以其实界面的绘制是需要遍历很多次的,如果页面层次太过复杂,每一帧需要刷新的 View 又很多时,耗时就会长一点。

当然,测量、布局、绘制这些流程在遍历时并不一定会把整颗 View 树都遍历一遍,ViewGroup 在传递这些流程时,还会再根据相应状态判断是否需要继续往下传递。

了解了 performTraversals() 是刷新界面的源头后,接下去就需要了解下它是什么时候执行的,和 scheduleTraversals() 又是什么关系?

performTraversals() 是在 doTraversal() 中被调用的,而 doTraversal() 又被封装到一个 Runnable 里,那么关键就是这个 Runnable 什么时候被执行了?

Choreographer

scheduleTraversals() 里调用了 Choreographer 的 postCallback() 将 Runnable 作为参数传了进去,所以跟进去看看:

//Choreograhper#postCallback
public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}
//Choreograhper#postCallbackDelayed
pubic void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) {
    ...  

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

//Choreograhper#postCallbackDelayedInternal
private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) {
    ...

    synchronized (mLock) {
        //1.获取当前时间戳
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        //2.根据时间戳将Runnable任务添加到指定的队列中
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        //3.因为postCallback默认传入delay = 0,所以代码会走进if里面
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {...}
    }
}

因为 postCallback() 调用 postCallbackDelayed() 时传了 delay = 0 进去,所以在 postCallbackDelayedInternal() 里面会先根据当前时间戳将这个 Runnable 保存到一个 mCallbackQueue 队列里,这个队列跟 MessageQueue 很相似,里面待执行的任务都是根据一个时间戳来排序。然后走了 scheduleFrameLocked() 方法这边,看看做了些什么:

//Choreograhper#scheduleFrameLocked
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        //1.系统4.0之后该变量默认为true,所以会走进if里
        if (USE_VSYNC) {
            ...

            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } ...
    }
}

如果代码走了 else 这边来发送一个消息,那么这个消息做的事肯定很重要,因为对这个 Message 设置了异步的标志而且用了sendMessageAtFrontOfQueue() 方法,这个方法是将这个 Message 直接放到 MessageQueue 队列里的头部,可以理解成设置了这个 Message 为最高优先级,那么先看看这个 Message 做了些什么:

//Choreograhper$FrameHandler#handleMessage
private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            ...
            case MSG_DO_SCHEDULE_VSYNC:
                doScheduleVsync();
                break;
            ...
        }
    }
}

//Choreographer#doScheduleVsync
void doScheduleVsync() {
    synchronized (mLock) {
        if (mFrameScheduled) {
            scheduleVsyncLocked();
        }
    }
}

所以这个 Message 最后做的事就是 scheduleVsyncLocked()。我们回到 scheduleFrameLocked() 这个方法里,当走 if 里的代码时,直接调用了 scheduleVsyncLocked(),当走 else 里的代码时,发了一个最高优先级的 Message,这个 Message 也是执行 scheduleVsyncLocked()。既然两边最后调用的都是同一个方法,那么为什么这么做呢?

关键在于 if 条件里那个方法,我的理解那个方法是用来判断当前是否是在主线程的,我们知道主线程也是一直在执行着一个个的 Message,那么如果在主线程的话,直接调用这个方法,那么这个方法就可以直接被执行了,如果不是在主线程,那么 post 一个最高优先级的 Message 到主线程去,保证这个方法可以第一时间得到处理。

那么这个方法是干嘛的呢,为什么需要在最短时间内被执行呢,而且只能在主线程?

//Choreographer#scheduleVsyncLocked
private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}
//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 层的一个方法,那跟到这里就跟不下去了。

那到这里,我们先来梳理一下:

到这里为止,我们知道一个 View 发起刷新的操作时,会层层通知到 ViewRootImpl 的 scheduleTraversals() 里去,然后这个方法会将遍历绘制 View 树的操作 performTraversals() 封装到 Runnable 里,传给 Choreographer,以当前的时间戳放进一个 mCallbackQueue 队列里,然后调用了 native 层的一个方法就跟不下去了。所以这个 Runnable 什么时候会被执行还不清楚。那么,下去的重点就是搞清楚它什么时候从队列里被拿出来执行了?

接下去只能换种方式继续跟了,既然这个 Runnable 操作被放在一个 mCallbackQueue 队列里,那就从这个队列着手,看看这个队列的取操作在哪被执行了:

//Choreographer$CallbackQueue
private final class CallbackQueue {
    private CallbackRecord mHead;

    ...
    //1.取操作
    public CallbackRecord extractDueCallbacksLocked(long now){...}  
    //2.入队列操作
    public void addCallbackLocked(long dueTime, Object action, Object token) {...}
    ...  
}

//Choreographer#doCallbacks
void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    synchronized(mLock) {
        ...

        //1.这个队列跟MessageQueue很相似,所以取的时候需要传入一个时间戳,因为队头的任务可能还没到设定的执行时间
        callback = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
        ...
    }
}

//Choreographer#doFrame
void doFrame(long frameTimeNanos, int frame) {
    ...

    try {
        ...
        //1.这个参数跟 ViewRootImpl调用mChoreographer.postCallback()时传进的第一个参数是一致的
        doCallbacks(Choreograhper.CALLBACK_TRAVERSAL, frameTimeNanos);
        ...
    }...
}

还记得我们说过在 ViewRootImpl 的 scheduleTraversals() 里会将遍历 View 树绘制的操作封装到 Runnable 里,然后调用 Choreographer 的 postCallback() 将这个 Runnable 放进队列里么,而当时调用 postCallback() 时传入了多个参数,这是因为 Choreographer 里有多个队列,而第一个参数 Choreographer.CALLBACK_TRAVERSAL 这个参数是用来区分队列的,可以理解成各个队列的 key 值。

那么这样一来,就找到关键的方法了:doFrame(),这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是 ViewRootImpl 封装起来的 doTraversal() 操作,而 doTraversal() 会去调用 performTraversals() 开始根据需要测量、布局、绘制整颗 View 树。所以剩下的问题就是 doFrame() 这个方法在哪里被调用了。

有几个调用的地方,但有个地方很关键:

//Choreographer$FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
    ...

    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        ...

        //1.这个这里的this,该message做的事其实是下面的run()方法
        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);
    }
}

关键的地方来了,这个继承自 DisplayEventReceiver 的 FrameDisplayEventReceiver 类的作用很重要。跟进去看注释,我只能理解它是用来接收底层信号用的。但看了网上的解释后,所有的都理解过来了:

FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。

也就是说,onVsync() 是底层会回调的,可以理解成每隔 16.6ms 一个帧信号来的时候,底层就会回调这个方法,当然前提是我们得先注册,这样底层才能找到我们 app 并回调。当这个方法被回调时,内部发起了一个 Message,注意看代码对这个 Message 设置了 callback 为 this,Handler 在处理消息时会先查看 Message 是否有 callback,有则优先交由 Message 的 callback 处理消息,没有的话再去看看Handler 有没有 callback,如果也没有才会交由 handleMessage() 这个方法执行。

这里这么做的原因,我猜测可能 onVsync() 是由底层回调的,那么它就不是运行在我们 app 的主线程上,毕竟上层 app 对底层是隐藏的。但这个 doFrame() 是个 ui 操作,它需要在主线程中执行,所以才通过 Handler 切到主线程中。

还记得我们前面分析 scheduleTraversals() 方法时,最后跟到了一个 native 层方法就跟不下去了么,现在再回过来想想这个 native 层方法的作用是什么,应该就比较好猜测了。

//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);
    }
}

英文不大理解,大体上可能是说安排接收一个 vsync 信号。而根据我们的分析,如果这个 vsync 信号发出的话,底层就会回调 DisplayEventReceiver 的 onVsync() 方法。

那如果只是这样的话,就有一点说不通了,首先上层 app 对于这些发送 vsync 信号的底层来说肯定是隐藏的,也就是说底层它根本不知道上层 app 的存在,那么在它的每 16.6ms 的帧信号来的时候,它是怎么找到我们的 app,并回调它的方法呢?

这就有点类似于观察者模式,或者说发布-订阅模式。既然上层 app 需要知道底层每隔 16.6ms 的帧信号事件,那么它就需要先注册监听才对,这样底层在发信号的时候,直接去找这些观察者通知它们就行了。

还有一点,scheduleVsync() 注册的监听应该只是监听下一个屏幕刷新信号的事件而已,而不是监听所有的屏幕刷新信号。比如说当前监听了第一帧的刷新信号事件,那么当第一帧的刷新信号来的时候,上层 app 就能接收到事件并作出反应。但如果还想监听第二帧的刷新信号,那么只能等上层 app 接收到第一帧的刷新信号之后再去监听下一帧。

虽然现在能力还不足以跟踪到 native 层,这些结论虽然是猜测的,但都经过调试,对注释、代码理解之后梳理出来的结论,跟原理应该不会偏差太多,这样子的理解应该是可以的。

1、我们知道一个 View 发起刷新的操作时,最终是走到了 ViewRootImpl 的 scheduleTraversals() 里去,然后这个方法会将遍历绘制 View 树的操作 performTraversals() 封装到 Runnable 里,传给 Choreographer,以当前的时间戳放进一个 mCallbackQueue 队列里,然后调用了 native 层的方法向底层注册监听下一个屏幕刷新信号事件。

2、当下一个屏幕刷新信号发出的时候,如果我们 app 有对这个事件进行监听,那么底层它就会回调我们 app 层的 onVsync() 方法来通知。当 onVsync() 被回调时,会发一个 Message 到主线程,将后续的工作切到主线程来执行。

3、切到主线程的工作就是去 mCallbackQueue 队列里根据时间戳将之前放进去的 Runnable 取出来执行,而这些 Runnable 有一个就是遍历绘制 View 树的操作 performTraversals()。在这次的遍历操作中,就会去绘制那些需要刷新的 View。

4、所以说,当我们调用了 invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过 ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过 performTraversals() 遍历绘制 View 树来执行这些刷新操作。

过滤一帧内重复的刷新请求

整体上的流程我们已经梳理出来的,但还有几点问题需要解决。我们在一个 16.6ms 的一帧内,代码里可能会有多个 View 发起了刷新请求,这是非常常见的场景了,比如某个动画是有多个 View 一起完成,比如界面发生了滑动等等。

按照我们上面梳理的流程,只要 View 发起了刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里去,是吧。而这个方法又会封装一个遍历绘制 View 树的操作 performTraversals() 到 Runnable 然后扔到队列里等刷新信号来的时候取出来执行,没错吧。

那如果多个 View 发起了刷新请求,岂不是意味着会有多次遍历绘制 View 树的操作?

其实,这点不用担心,还记得我们在最开始分析 scheduleTraverslas() 的时候先跳过了一些代码么?现在我们回过来继续看看这些代码:

//ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        //1.注意这个boolean类型的变量
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
            Choreograhper.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

我们上面分析的 scheduleTraversals() 干的那一串工作,前提是 mTraversalScheduled 这个 boolean 类型变量等于 false 才会去执行。那这个变量在什么时候被赋值被 false 了呢:

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

只有三个被赋值为 false 的地方,一个是上图的 doTraversal(),还有就是声明时默认为 false,剩下一个是在取消遍历绘制 View 操作 unscheduleTraversals() 里。这两个可以先不去看,就看看 doTraversal()。还记得这个方法吧,就是在 scheduleTraversals() 中封装到 Runnable 里的那个方法。

也就是说,当我们调用了一次 scheduleTraversals()之后,直到下一个屏幕刷新信号来的时候,doTraversal() 被取出来执行。在这期间重复调用 scheduleTraversals() 都会被过滤掉的。那么为什么需要这样呢?

其实,想想就能明白了。View 最终是怎么刷新的呢,就是在执行 performTraversals() 遍历绘制 View 树过程中层层遍历到需要刷新的 View,然后去绘制它的吧。既然是遍历,那么不管上一帧内有多少个 View 发起了刷新的请求,在这一次的遍历过程中全部都会去处理的吧。这也是我们从代码上看到的,每一个屏幕刷新信号来的时候,只会去执行一次 performTraversals(),因为只需遍历一遍,就能够刷新所有的 View 了。

而 performTraversals() 会被执行的前提是调用了 scheduleTraversals() 来向底层注册监听了下一个屏幕刷新信号事件,所以在同一个 16.6ms 的一帧内,只需要第一个发起刷新请求的 View 来走一遍 scheduleTraversals() 干的事就可以了,其他不管还有多少 View 发起了刷新请求,没必要再去重复向底层注册监听下一个屏幕刷新信号事件了,反正只要有一次遍历绘制 View 树的操作就可以对它们进行刷新了。

postSyncBarrier()---同步屏障消息

还剩最后一个问题,scheduleTraversals() 里我们还有一行代码没分析。这个问题是这样的:

我们清楚主线程其实是一直在处理 MessageQueue 消息队列里的 Message,每个操作都是一个 Message,打开 Activity 是一个 Message,遍历绘制 View 树来刷新屏幕也是一个 Message。

而且,上面梳理完我们也清楚,遍历绘制 View 树的操作是在屏幕刷新信号到的时候,底层回调我们 app 的 onVsync(),这个方法再去将遍历绘制 View 树的操作 post 到主线程的 MessageQueue 中去等待执行。主线程同一时间只能处理一个 Message,这些 Message 就肯定有先后的问题,那么会不会出现下面这种情况呢:

image

也就是说,当我们的 app 接收到屏幕刷新信号时,来不及第一时间就去执行刷新屏幕的操作,这样一来,即使我们将布局优化得很彻底,保证绘制当前 View 树不会超过 16ms,但如果不能第一时间优先处理绘制 View 的工作,那等 16.6 ms 过了,底层需要去切换下一帧的画面了,我们 app 却还没处理完,这样也照样会出现丢帧了吧。而且这种场景是非常有可能出现的吧,毕竟主线程需要处理的事肯定不仅仅是刷新屏幕的事而已,那么这个问题是怎么处理的呢?

所以我们继续回来看 scheduleTraversals():

//ViewRootImpl#scheduleTraversalsvoid scheduleTraversals() {    if (!mTraversalScheduled) {        mTraversalScheduled = true;        //1.注意这行代码,往主线程的消息队列里发送了一个同步屏障消息        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();        mChoreographer.postCallback(            Choreograhper.CALLBACK_TRAVERSAL, mTraversalRunnable, null);        ...    }}//ViewRootImpl#doTraversalvoid doTraversal() {    if (mTraversalScheduled) {        mTraversalScheduled = false;        //1.注意这行代码,移除消息队列里的同步屏障消息        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);        ...        performTraversals();        ...    }}

在逻辑走进 Choreographer 前会先往队列里发送一个同步屏障,而当 doTraversal() 被调用时才将同步屏障移除。这个同步屏障又涉及到消息机制了,不深入了,这里就只给出结论。

这个同步屏障的作用可以理解成拦截同步消息的执行,主线程的 Looper 会一直循环调用 MessageQueue 的 next() 来取出队头的 Message 执行,当 Message 执行完后再去取下一个。当 next() 方法在取 Message 时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,如果有找到异步消息,那么就取出这个异步消息来执行,否则就让 next() 方法陷入阻塞状态。如果 next() 方法陷入阻塞状态,那么主线程此时就是处于空闲状态的,也就是没在干任何事。所以,如果队头是一个同步屏障的消息的话,那么在它后面的所有同步消息就都被拦截住了,直到这个同步屏障消息被移除出队列,否则主线程就一直不会去处理同步屏幕后面的同步消息。

而所有消息默认都是同步消息,只有手动设置了异步标志,这个消息才会是异步消息。另外,同步屏障消息只能由内部来发送,这个接口并没有公开给我们使用。

最后,仔细看上面 Choreographer 里所有跟 message 有关的代码,你会发现,都手动设置了异步消息的标志,所以这些操作是不受到同步屏障影响的。这样做的原因可能就是为了尽可能保证上层 app 在接收到屏幕刷新信号时,可以在第一时间执行遍历绘制 View 树的工作。

因为主线程中如果有太多消息要执行,而这些消息又是根据时间戳进行排序,如果不加一个同步屏障的话,那么遍历绘制 View 树的工作就可能被迫延迟执行,因为它也需要排队,那么就有可能出现当一帧都快结束的时候才开始计算屏幕数据,那即使这次的计算少于 16.6ms,也同样会造成丢帧现象。

那么,有了同步屏障消息的控制就能保证每次一接收到屏幕刷新信号就第一时间处理遍历绘制 View 树的工作么?

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

总结

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

  2. scheduleTraversals() 会先过滤掉同一帧内的重复调用,在同一帧内只需要安排一次遍历绘制 View 树的任务即可,这个任务会在下一个屏幕刷新信号到来时调用 performTraversals() 遍历 View 树,遍历过程中会将所有需要刷新的 View 进行重绘;

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

  4. 发完同步屏障后 scheduleTraversals() 才会开始安排一个遍历绘制 View 树的操作,作法是把 performTraversals() 封装到 Runnable 里面,然后调用 Choreographer 的 postCallback() 方法;

  5. postCallback() 方法会先将这个 Runnable 任务以当前时间戳放进一个待执行的队列里,然后如果当前是在主线程就会直接调用一个native 层方法,如果不是在主线程,会发一个最高优先级的 message 到主线程,让主线程第一时间调用这个 native 层的方法;

  6. native 层的这个方法是用来向底层注册监听下一个屏幕刷新信号,当下一个屏幕刷新信号发出时,底层就会回调 Choreographer 的onVsync() 方法来通知上层 app;

  7. onVsync() 方法被回调时,会往主线程的消息队列中发送一个执行 doFrame() 方法的消息,这个消息是异步消息,所以不会被同步屏障拦截住;

  8. doFrame() 方法会去取出之前放进待执行队列里的任务来执行,取出来的这个任务实际上是 ViewRootImpl 的 doTraversal() 操作;

  9. 上述第4步到第8步涉及到的消息都手动设置成了异步消息,所以不会受到同步屏障的拦截;

  10. doTraversal() 方法会先移除主线程的同步屏障,然后调用 performTraversals() 开始根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程,在这几个流程中都会去遍历 View 树来刷新需要更新的View;

再来一张时序图结尾,大伙想自己过源码时可以跟着时序图来,建议在电脑上阅读:

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容