Android屏幕刷新機(jī)制解析(轉(zhuǎn))

android屏幕刷新顯示機(jī)制

在一個(gè)典型的顯示系統(tǒng)中,一般包括CPU、GPU、屏幕三個(gè)部分, CPU負(fù)責(zé)計(jì)算數(shù)據(jù),把計(jì)算好數(shù)據(jù)交給GPU,GPU會對圖形數(shù)據(jù)進(jìn)行渲染,渲染好后放到buffer里存起來,然后屏幕負(fù)責(zé)把buffer里的數(shù)據(jù)呈現(xiàn)到屏幕上。

顯示過程,簡單的說就是CPU/GPU準(zhǔn)備好數(shù)據(jù),存入buffer,屏幕每隔一段時(shí)間去buffer里取數(shù)據(jù),然后顯示出來。屏幕讀取的頻率是固定的,但是CPU/GPU寫數(shù)據(jù)是完全無規(guī)律的。

上述內(nèi)容概括一下,大體意思就是說,屏幕的刷新包括三個(gè)步驟:CPU 計(jì)算屏幕數(shù)據(jù)、GPU 進(jìn)一步處理和緩存、最后 display 再將緩存中(buffer)的屏幕數(shù)據(jù)顯示出來

對于 Android 而言,第一個(gè)步驟:CPU 計(jì)算屏幕數(shù)據(jù)指的也就是 View 樹的繪制過程,也就是 Activity 對應(yīng)的視圖樹從根布局 DecorView 開始層層遍歷每個(gè) View,分別執(zhí)行測量、布局、繪制三個(gè)操作的過程。

也就是說,我們常說的 Android 每隔 16.6ms 刷新一次屏幕其實(shí)是指:底層以固定的頻率,比如每 16.6ms 將 buffer 里的屏幕數(shù)據(jù)顯示出來

如果還不清楚,那再看一張網(wǎng)上很常見的圖:

image

Display 這一行可以理解成屏幕,所以可以看到,底層是以固定的頻率發(fā)出 VSync 信號的,而這個(gè)固定頻率就是我們常說的每 16.6ms 發(fā)送一個(gè) VSync 信號。Display 黃色的這一行里有一些數(shù)字:0, 1, 2, 3, 4,可以看到每次屏幕刷新信號到了的時(shí)候,數(shù)字就會變化,所以這些數(shù)字其實(shí)可以理解成每一幀屏幕顯示的畫面。也就是說,屏幕每一幀的畫面可以持續(xù) 16.6ms,當(dāng)過了 16.6ms,底層就會發(fā)出一個(gè)屏幕刷新信號,而屏幕就會去顯示下一幀的畫面。

接下去就還是看這圖,然后講講我們 app 層該干的事了:

繼續(xù)看圖,CPU 藍(lán)色這段時(shí)間就跟我們自己寫的代碼有關(guān)系了,如果你的布局很復(fù)雜,層次嵌套很多,每一幀內(nèi)需要刷新的 View 又很多時(shí),那么每一幀的繪制耗時(shí)自然就會多一點(diǎn)。

繼續(xù)看圖,CPU 藍(lán)色這行里也有一些數(shù)字,其實(shí)這些數(shù)字跟 Display 黃色的那一行里的數(shù)字是對應(yīng)的,在 Display 里我們解釋過這些數(shù)字表示的是每一幀的畫面,那么在 CPU 這一行里,其實(shí)就是在計(jì)算對應(yīng)幀的畫面數(shù)據(jù),也叫屏幕數(shù)據(jù)。也就是說,在當(dāng)前幀內(nèi),CPU 是在計(jì)算下一幀的屏幕畫面數(shù)據(jù),當(dāng)屏幕刷新信號到的時(shí)候,屏幕就去將 CPU 計(jì)算的屏幕畫面數(shù)據(jù)顯示出來;同時(shí) CPU 也接收到屏幕刷新信號,所以也開始去計(jì)算下一幀的屏幕畫面數(shù)據(jù)。

CPU 跟 Display 是不同的硬件,它們是可以并行工作的。要理解的一點(diǎn)是,我們寫的代碼,只是控制讓 CPU 在接收到屏幕刷新信號的時(shí)候開始去計(jì)算下一幀的畫面工作。而底層在每一次屏幕刷新信號來的時(shí)候都會去切換這一幀的畫面,這點(diǎn)我們是控制不了的,是底層的工作機(jī)制。之所以要講這點(diǎn),是因?yàn)?,?dāng)我們的 app 界面沒有必要再刷新時(shí),我們 app 是接收不到屏幕刷新信號的,所以也就不會讓 CPU 去計(jì)算下一幀畫面數(shù)據(jù),但是底層仍然會以固定的頻率來切換每一幀的畫面,只是它后面切換的每一幀畫面都一樣,所以給我們的感覺就是屏幕沒刷新。

那么我們就先稍微來梳理一下:

1、我們常說的 Android 每隔 16.6 ms 刷新一次屏幕其實(shí)是指底層會以這個(gè)固定頻率來切換每一幀的畫面。

2、這個(gè)每一幀的畫面也就是我們的 app 繪制視圖樹(View 樹)計(jì)算而來的,這個(gè)工作是交由 CPU 處理,耗時(shí)的長短取決于我們寫的代碼。

3、CPU 繪制視圖樹來計(jì)算下一幀畫面數(shù)據(jù)的工作是在屏幕刷新信號來的時(shí)候才開始工作的,而當(dāng)這個(gè)工作處理完畢后,也就是下一幀的畫面數(shù)據(jù)已經(jīng)全部計(jì)算完畢,也不會馬上顯示到屏幕上,而是會等下一個(gè)屏幕刷新信號來的時(shí)候再交由底層將計(jì)算完畢的屏幕畫面數(shù)據(jù)顯示出來。

4、當(dāng)我們的 app 界面不需要刷新時(shí)(用戶無操作,界面無動(dòng)畫),app 就接收不到屏幕刷新信號所以也就不會讓 CPU 再去繪制視圖樹計(jì)算畫面數(shù)據(jù)工作,但是底層仍然會每隔 16.6 ms 切換下一幀的畫面,只是這個(gè)下一幀畫面一直是相同的內(nèi)容。

ViewRootImpl 與 DecorView 的綁定

View#invalidate() 是請求重繪的一個(gè)操作,我們跟著 invalidate() 一步步往下走的時(shí)候,發(fā)現(xiàn)最后跟到了 ViewRootImpl#scheduleTraversals() 就停止了。而 ViewRootImpl 就是今天我們要介紹的重點(diǎn)對象了。

Android 設(shè)備呈現(xiàn)到界面上的大多數(shù)情況下都是一個(gè) Activity,真正承載視圖的是一個(gè) Window,每個(gè) Window 都有一個(gè) DecorView,我們調(diào)用 setContentView() 其實(shí)是將我們自己寫的布局文件添加到以 DecorView 為根布局的一個(gè) ViewGroup 里,構(gòu)成一顆 View 樹。但其實(shí) DecorView 還有 mParent,而且就是 ViewRootImpl,而且每個(gè)界面上的 View 的刷新,繪制,點(diǎn)擊事件的分發(fā)其實(shí)都是由 ViewRootImpl 作為發(fā)起者的,由 ViewRootImpl 控制這些操作從 DecorView 開始遍歷 View 樹去分發(fā)處理。

為什么 DecorView 的 mParent 會是 ViewRootImpl 呢?換個(gè)問法也就是,在什么時(shí)候?qū)?DevorView 和 ViewRootImpl 綁定起來?

Activity 的啟動(dòng)是在 ActivityThread 里完成的,handleLaunchActivity() 會依次間接的執(zhí)行到 Activity 的 onCreate(), onStart(), onResume()。在執(zhí)行完這些后 ActivityThread 會調(diào)用 WindowManager#addView(),而這個(gè) addView() 最終其實(shí)是調(diào)用了 WindowManagerGlobal 的 addView() 方法,我們就從這里開始看:


//WindowManagerGlobal#addView

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

...

ViewRootImpl root;

...

synchronized(mLock) {

...

//1\. 實(shí)例化一個(gè) ViewRootImpl對象

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

...

mViews.add(view);

mRoots.add(root);

...

}

try{

//2\. 調(diào)用ViewRootImpl的setView(),并將DecorView作為參數(shù)傳遞進(jìn)去

root.setView(view, wparams, panelParentView);

}...

}

WindowManager 維護(hù)著所有 Activity 的 DecorView 和 ViewRootImpl。這里初始化了一個(gè) ViewRootImpl,然后調(diào)用了它的 setView() 方法,將 DevorView 作為參數(shù)傳遞了進(jìn)去。所以看看 ViewRootImpl 中的 setView() 做了什么:


//ViewRootImpl#setView

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

synchronized(this) {

if(mView ==null) {

//1\. view 是 DecorView

mView = view;

...

//2.發(fā)起布局請求

requestLayout();

...

//3.將當(dāng)前ViewRootImpl對象this,作為參數(shù)調(diào)用了DecorView的assignParent

view.assignParent(this);

...

}

}

}

在 setView() 方法里調(diào)用了 DecorView 的 assignParent() 方法,所以去看看 View 的這個(gè)方法:


//View#assignParent

voidassignParent(ViewParent parent){

if(mParent ==null) {

mParent =null;

}elseif(parent ==null) {

mParent =null;

}else{

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

}

}

參數(shù)是 ViewParent,而 ViewRootImpl 是實(shí)現(xiàn)了 ViewParent 接口的,所以在這里就將 DecorView 和 ViewRootImpl 綁定起來了。每個(gè)Activity 的根布局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl。

跟界面刷新相關(guān)的方法里應(yīng)該都會有一個(gè)循環(huán)找 parent 的方法,或者是不斷調(diào)用 parent 的方法,這樣最終才都會走到 ViewRootImpl 里,也就是說實(shí)際上 View 的刷新都是由 ViewRootImpl 來控制的。

即使是界面上一個(gè)小小的 View 發(fā)起了重繪請求時(shí),都要層層走到 ViewRootImpl,由它來發(fā)起重繪請求,然后再由它來開始遍歷 View 樹,一直遍歷到這個(gè)需要重繪的 View 再調(diào)用它的 onDraw() 方法進(jìn)行繪制。

我們重新看回 ViewRootImpl 的 setView() 這個(gè)方法,這個(gè)方法里還調(diào)用了一個(gè) requestLayout() 方法:


//ViewRootImpl#requestLayout

@Override

publicvoidrequestLayout(){

if(!mHandingLayoutInLayoutRequest) {

//1.檢查該操作是否是在主線程中執(zhí)行

checkThread();

mLayoutRequested =true;

//2.安排一次遍歷繪制View樹的任務(wù)

scheduleTraversals();

}

}

這里調(diào)用了一個(gè) scheduleTraversals(),還記得當(dāng) View 發(fā)起重繪操作 invalidate() 時(shí),最后也調(diào)用了 scheduleTraversals() 這個(gè)方法么。其實(shí)這個(gè)方法就是屏幕刷新的關(guān)鍵,它是安排一次繪制 View 樹的任務(wù)等待執(zhí)行。

也就是說,其實(shí)打開一個(gè) Activity,當(dāng)它的 onCreate---onResume 生命周期都走完后,才將它的 DecoView 與新建的一個(gè) ViewRootImpl 對象綁定起來,同時(shí)開始安排一次遍歷 View 任務(wù)也就是繪制 View 樹的操作等待執(zhí)行,然后將 DecoView 的 parent 設(shè)置成 ViewRootImpl 對象。

這也就是為什么在 onCreate---onResume 里獲取不到 View 寬高的原因,因?yàn)樵谶@個(gè)時(shí)刻 ViewRootImpl 甚至都還沒創(chuàng)建,更不用說是否已經(jīng)執(zhí)行過測量操作了。

還可以得到一點(diǎn)信息是,一個(gè) Activity 界面的繪制,其實(shí)是在 onResume() 之后才開始的。

ViewRootImpl#scheduleTraversals

到這里,我們梳理清楚了,調(diào)用一個(gè) View 的 invalidate() 請求重繪操作,內(nèi)部原來是要層層通知到 ViewRootImpl 的 scheduleTraversals() 里去。而且打開一個(gè)新的 Activity,它的界面繪制原來是在 onResume() 之后也層層通知到 ViewRootImpl 的 scheduleTraversals() 里去。雖然其他關(guān)于 View 的刷新操作,比如 requestLayout() 等等之類的方法我們還沒有去看,但我們已經(jīng)可以大膽猜測,這些跟 View 刷新有關(guān)的操作最終也都會層層走到 ViewRootImpl 中的 scheduleTraversals() 方法里去的。

那么這個(gè)方法究竟干了些什么,我們就要好好來分析了:


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

mTraversalScheduled 這個(gè) boolean 變量的作用等會再來看,先看看 mChoreographer.postCallback() 這個(gè)方法,傳入了三個(gè)參數(shù),第二個(gè)參數(shù)是一個(gè) Runnable 對象,先來看看這個(gè) Runnable:

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

//ViewRootImpl成員變量
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
這個(gè) Runnable 做的事很簡單,就調(diào)用了一個(gè)方法,doTraversal():

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

        //1. 遍歷繪制View樹
        performTraversals();
        ...
    }
}

看看這個(gè)方法做的事,跟 scheduleTraversals() 正好相反,一個(gè)將變量置成 true,這里置成 false,一個(gè)是 postSyncBarrier(),這里是 removeSyncBarrier(),具體作用等會再說,繼續(xù)先看看 performTraversals(),這個(gè)方法也是屏幕刷新的關(guān)鍵:

//ViewRootImpl#performTraversals
private void performTraversals() {
    //該方法實(shí)在太過復(fù)雜,所以將無關(guān)代碼全部都省略掉,只留下關(guān)鍵代碼和代碼結(jié)構(gòu)
    ...
    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 發(fā)起,而且還都是在 performTraversals() 方法中發(fā)起的,所以這個(gè)方法的邏輯很復(fù)雜,因?yàn)槊看味夹枰鶕?jù)相應(yīng)狀態(tài)判斷是否需要三個(gè)流程都走,有時(shí)可能只需要執(zhí)行 performDraw() 繪制流程,有時(shí)可能只執(zhí)行 performMeasure() 測量和 performLayout() 布局流程(一般測量和布局流程是一起執(zhí)行的)。不管哪個(gè)流程都會遍歷一次 View 樹,所以其實(shí)界面的繪制是需要遍歷很多次的,如果頁面層次太過復(fù)雜,每一幀需要刷新的 View 又很多時(shí),耗時(shí)就會長一點(diǎn)。

當(dāng)然,測量、布局、繪制這些流程在遍歷時(shí)并不一定會把整顆 View 樹都遍歷一遍,ViewGroup 在傳遞這些流程時(shí),還會再根據(jù)相應(yīng)狀態(tài)判斷是否需要繼續(xù)往下傳遞。

了解了 performTraversals() 是刷新界面的源頭后,接下去就需要了解下它是什么時(shí)候執(zhí)行的,和 scheduleTraversals() 又是什么關(guān)系?

performTraversals() 是在 doTraversal() 中被調(diào)用的,而 doTraversal() 又被封裝到一個(gè) Runnable 里,那么關(guān)鍵就是這個(gè) Runnable 什么時(shí)候被執(zhí)行了?

Choreographer

scheduleTraversals() 里調(diào)用了 Choreographer 的 postCallback() 將 Runnable 作為參數(shù)傳了進(jìn)去,所以跟進(jìn)去看看:

//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.獲取當(dāng)前時(shí)間戳
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        //2.根據(jù)時(shí)間戳將Runnable任務(wù)添加到指定的隊(duì)列中
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        //3.因?yàn)閜ostCallback默認(rèn)傳入delay = 0,所以代碼會走進(jìn)if里面
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {...}
    }
}

因?yàn)?postCallback() 調(diào)用 postCallbackDelayed() 時(shí)傳了 delay = 0 進(jìn)去,所以在 postCallbackDelayedInternal() 里面會先根據(jù)當(dāng)前時(shí)間戳將這個(gè) Runnable 保存到一個(gè) mCallbackQueue 隊(duì)列里,這個(gè)隊(duì)列跟 MessageQueue 很相似,里面待執(zhí)行的任務(wù)都是根據(jù)一個(gè)時(shí)間戳來排序。然后走了 scheduleFrameLocked() 方法這邊,看看做了些什么:

//Choreograhper#scheduleFrameLocked
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        //1.系統(tǒng)4.0之后該變量默認(rèn)為true,所以會走進(jìn)if里
        if (USE_VSYNC) {
            ...

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

如果代碼走了 else 這邊來發(fā)送一個(gè)消息,那么這個(gè)消息做的事肯定很重要,因?yàn)閷@個(gè) Message 設(shè)置了異步的標(biāo)志而且用了sendMessageAtFrontOfQueue() 方法,這個(gè)方法是將這個(gè) Message 直接放到 MessageQueue 隊(duì)列里的頭部,可以理解成設(shè)置了這個(gè) Message 為最高優(yōu)先級,那么先看看這個(gè) 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();
        }
    }
}

所以這個(gè) Message 最后做的事就是 scheduleVsyncLocked()。我們回到 scheduleFrameLocked() 這個(gè)方法里,當(dāng)走 if 里的代碼時(shí),直接調(diào)用了 scheduleVsyncLocked(),當(dāng)走 else 里的代碼時(shí),發(fā)了一個(gè)最高優(yōu)先級的 Message,這個(gè) Message 也是執(zhí)行 scheduleVsyncLocked()。既然兩邊最后調(diào)用的都是同一個(gè)方法,那么為什么這么做呢?

關(guān)鍵在于 if 條件里那個(gè)方法,我的理解那個(gè)方法是用來判斷當(dāng)前是否是在主線程的,我們知道主線程也是一直在執(zhí)行著一個(gè)個(gè)的 Message,那么如果在主線程的話,直接調(diào)用這個(gè)方法,那么這個(gè)方法就可以直接被執(zhí)行了,如果不是在主線程,那么 post 一個(gè)最高優(yōu)先級的 Message 到主線程去,保證這個(gè)方法可以第一時(shí)間得到處理。

那么這個(gè)方法是干嘛的呢,為什么需要在最短時(shí)間內(nèi)被執(zhí)行呢,而且只能在主線程?

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

調(diào)用了 native 層的一個(gè)方法,那跟到這里就跟不下去了。

那到這里,我們先來梳理一下:

到這里為止,我們知道一個(gè) View 發(fā)起刷新的操作時(shí),會層層通知到 ViewRootImpl 的 scheduleTraversals() 里去,然后這個(gè)方法會將遍歷繪制 View 樹的操作 performTraversals() 封裝到 Runnable 里,傳給 Choreographer,以當(dāng)前的時(shí)間戳放進(jìn)一個(gè) mCallbackQueue 隊(duì)列里,然后調(diào)用了 native 層的一個(gè)方法就跟不下去了。所以這個(gè) Runnable 什么時(shí)候會被執(zhí)行還不清楚。那么,下去的重點(diǎn)就是搞清楚它什么時(shí)候從隊(duì)列里被拿出來執(zhí)行了?

接下去只能換種方式繼續(xù)跟了,既然這個(gè) Runnable 操作被放在一個(gè) mCallbackQueue 隊(duì)列里,那就從這個(gè)隊(duì)列著手,看看這個(gè)隊(duì)列的取操作在哪被執(zhí)行了:

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

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

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

        //1.這個(gè)隊(duì)列跟MessageQueue很相似,所以取的時(shí)候需要傳入一個(gè)時(shí)間戳,因?yàn)殛?duì)頭的任務(wù)可能還沒到設(shè)定的執(zhí)行時(shí)間
        callback = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
        ...
    }
}

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

    try {
        ...
        //1.這個(gè)參數(shù)跟 ViewRootImpl調(diào)用mChoreographer.postCallback()時(shí)傳進(jìn)的第一個(gè)參數(shù)是一致的
        doCallbacks(Choreograhper.CALLBACK_TRAVERSAL, frameTimeNanos);
        ...
    }...
}

還記得我們說過在 ViewRootImpl 的 scheduleTraversals() 里會將遍歷 View 樹繪制的操作封裝到 Runnable 里,然后調(diào)用 Choreographer 的 postCallback() 將這個(gè) Runnable 放進(jìn)隊(duì)列里么,而當(dāng)時(shí)調(diào)用 postCallback() 時(shí)傳入了多個(gè)參數(shù),這是因?yàn)?Choreographer 里有多個(gè)隊(duì)列,而第一個(gè)參數(shù) Choreographer.CALLBACK_TRAVERSAL 這個(gè)參數(shù)是用來區(qū)分隊(duì)列的,可以理解成各個(gè)隊(duì)列的 key 值。

那么這樣一來,就找到關(guān)鍵的方法了:doFrame(),這個(gè)方法里會根據(jù)一個(gè)時(shí)間戳去隊(duì)列里取任務(wù)出來執(zhí)行,而這個(gè)任務(wù)就是 ViewRootImpl 封裝起來的 doTraversal() 操作,而 doTraversal() 會去調(diào)用 performTraversals() 開始根據(jù)需要測量、布局、繪制整顆 View 樹。所以剩下的問題就是 doFrame() 這個(gè)方法在哪里被調(diào)用了。

有幾個(gè)調(diào)用的地方,但有個(gè)地方很關(guān)鍵:

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

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

        //1.這個(gè)這里的this,該message做的事其實(shí)是下面的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);
    }
}

關(guān)鍵的地方來了,這個(gè)繼承自 DisplayEventReceiver 的 FrameDisplayEventReceiver 類的作用很重要。跟進(jìn)去看注釋,我只能理解它是用來接收底層信號用的。但看了網(wǎng)上的解釋后,所有的都理解過來了:

FrameDisplayEventReceiver繼承自DisplayEventReceiver接收底層的VSync信號開始處理UI過程。VSync信號由SurfaceFlinger實(shí)現(xiàn)并定時(shí)發(fā)送。FrameDisplayEventReceiver收到信號后,調(diào)用onVsync方法組織消息發(fā)送到主線程處理。這個(gè)消息主要內(nèi)容就是run方法里面的doFrame了,這里mTimestampNanos是信號到來的時(shí)間參數(shù)。

也就是說,onVsync() 是底層會回調(diào)的,可以理解成每隔 16.6ms 一個(gè)幀信號來的時(shí)候,底層就會回調(diào)這個(gè)方法,當(dāng)然前提是我們得先注冊,這樣底層才能找到我們 app 并回調(diào)。當(dāng)這個(gè)方法被回調(diào)時(shí),內(nèi)部發(fā)起了一個(gè) Message,注意看代碼對這個(gè) Message 設(shè)置了 callback 為 this,Handler 在處理消息時(shí)會先查看 Message 是否有 callback,有則優(yōu)先交由 Message 的 callback 處理消息,沒有的話再去看看Handler 有沒有 callback,如果也沒有才會交由 handleMessage() 這個(gè)方法執(zhí)行。

這里這么做的原因,我猜測可能 onVsync() 是由底層回調(diào)的,那么它就不是運(yùn)行在我們 app 的主線程上,畢竟上層 app 對底層是隱藏的。但這個(gè) doFrame() 是個(gè) ui 操作,它需要在主線程中執(zhí)行,所以才通過 Handler 切到主線程中。

還記得我們前面分析 scheduleTraversals() 方法時(shí),最后跟到了一個(gè) native 層方法就跟不下去了么,現(xiàn)在再回過來想想這個(gè) native 層方法的作用是什么,應(yīng)該就比較好猜測了。

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

英文不大理解,大體上可能是說安排接收一個(gè) vsync 信號。而根據(jù)我們的分析,如果這個(gè) vsync 信號發(fā)出的話,底層就會回調(diào) DisplayEventReceiver 的 onVsync() 方法。

那如果只是這樣的話,就有一點(diǎn)說不通了,首先上層 app 對于這些發(fā)送 vsync 信號的底層來說肯定是隱藏的,也就是說底層它根本不知道上層 app 的存在,那么在它的每 16.6ms 的幀信號來的時(shí)候,它是怎么找到我們的 app,并回調(diào)它的方法呢?

這就有點(diǎn)類似于觀察者模式,或者說發(fā)布-訂閱模式。既然上層 app 需要知道底層每隔 16.6ms 的幀信號事件,那么它就需要先注冊監(jiān)聽才對,這樣底層在發(fā)信號的時(shí)候,直接去找這些觀察者通知它們就行了。

還有一點(diǎn),scheduleVsync() 注冊的監(jiān)聽?wèi)?yīng)該只是監(jiān)聽下一個(gè)屏幕刷新信號的事件而已,而不是監(jiān)聽所有的屏幕刷新信號。比如說當(dāng)前監(jiān)聽了第一幀的刷新信號事件,那么當(dāng)?shù)谝粠乃⑿滦盘杹淼臅r(shí)候,上層 app 就能接收到事件并作出反應(yīng)。但如果還想監(jiān)聽第二幀的刷新信號,那么只能等上層 app 接收到第一幀的刷新信號之后再去監(jiān)聽下一幀。

雖然現(xiàn)在能力還不足以跟蹤到 native 層,這些結(jié)論雖然是猜測的,但都經(jīng)過調(diào)試,對注釋、代碼理解之后梳理出來的結(jié)論,跟原理應(yīng)該不會偏差太多,這樣子的理解應(yīng)該是可以的。

1、我們知道一個(gè) View 發(fā)起刷新的操作時(shí),最終是走到了 ViewRootImpl 的 scheduleTraversals() 里去,然后這個(gè)方法會將遍歷繪制 View 樹的操作 performTraversals() 封裝到 Runnable 里,傳給 Choreographer,以當(dāng)前的時(shí)間戳放進(jìn)一個(gè) mCallbackQueue 隊(duì)列里,然后調(diào)用了 native 層的方法向底層注冊監(jiān)聽下一個(gè)屏幕刷新信號事件。

2、當(dāng)下一個(gè)屏幕刷新信號發(fā)出的時(shí)候,如果我們 app 有對這個(gè)事件進(jìn)行監(jiān)聽,那么底層它就會回調(diào)我們 app 層的 onVsync() 方法來通知。當(dāng) onVsync() 被回調(diào)時(shí),會發(fā)一個(gè) Message 到主線程,將后續(xù)的工作切到主線程來執(zhí)行。

3、切到主線程的工作就是去 mCallbackQueue 隊(duì)列里根據(jù)時(shí)間戳將之前放進(jìn)去的 Runnable 取出來執(zhí)行,而這些 Runnable 有一個(gè)就是遍歷繪制 View 樹的操作 performTraversals()。在這次的遍歷操作中,就會去繪制那些需要刷新的 View。

4、所以說,當(dāng)我們調(diào)用了 invalidate(),requestLayout(),等之類刷新界面的操作時(shí),并不是馬上就會執(zhí)行這些刷新的操作,而是通過 ViewRootImpl 的 scheduleTraversals() 先向底層注冊監(jiān)聽下一個(gè)屏幕刷新信號事件,然后等下一個(gè)屏幕刷新信號來的時(shí)候,才會去通過 performTraversals() 遍歷繪制 View 樹來執(zhí)行這些刷新操作。

過濾一幀內(nèi)重復(fù)的刷新請求

整體上的流程我們已經(jīng)梳理出來的,但還有幾點(diǎn)問題需要解決。我們在一個(gè) 16.6ms 的一幀內(nèi),代碼里可能會有多個(gè) View 發(fā)起了刷新請求,這是非常常見的場景了,比如某個(gè)動(dòng)畫是有多個(gè) View 一起完成,比如界面發(fā)生了滑動(dòng)等等。

按照我們上面梳理的流程,只要 View 發(fā)起了刷新請求最終都會走到 ViewRootImpl 中的 scheduleTraversals() 里去,是吧。而這個(gè)方法又會封裝一個(gè)遍歷繪制 View 樹的操作 performTraversals() 到 Runnable 然后扔到隊(duì)列里等刷新信號來的時(shí)候取出來執(zhí)行,沒錯(cuò)吧。

那如果多個(gè) View 發(fā)起了刷新請求,豈不是意味著會有多次遍歷繪制 View 樹的操作?

其實(shí),這點(diǎn)不用擔(dān)心,還記得我們在最開始分析 scheduleTraverslas() 的時(shí)候先跳過了一些代碼么?現(xiàn)在我們回過來繼續(xù)看看這些代碼:

//ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        //1.注意這個(gè)boolean類型的變量
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
            Choreograhper.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

我們上面分析的 scheduleTraversals() 干的那一串工作,前提是 mTraversalScheduled 這個(gè) boolean 類型變量等于 false 才會去執(zhí)行。那這個(gè)變量在什么時(shí)候被賦值被 false 了呢:

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

只有三個(gè)被賦值為 false 的地方,一個(gè)是上圖的 doTraversal(),還有就是聲明時(shí)默認(rèn)為 false,剩下一個(gè)是在取消遍歷繪制 View 操作 unscheduleTraversals() 里。這兩個(gè)可以先不去看,就看看 doTraversal()。還記得這個(gè)方法吧,就是在 scheduleTraversals() 中封裝到 Runnable 里的那個(gè)方法。

也就是說,當(dāng)我們調(diào)用了一次 scheduleTraversals()之后,直到下一個(gè)屏幕刷新信號來的時(shí)候,doTraversal() 被取出來執(zhí)行。在這期間重復(fù)調(diào)用 scheduleTraversals() 都會被過濾掉的。那么為什么需要這樣呢?

其實(shí),想想就能明白了。View 最終是怎么刷新的呢,就是在執(zhí)行 performTraversals() 遍歷繪制 View 樹過程中層層遍歷到需要刷新的 View,然后去繪制它的吧。既然是遍歷,那么不管上一幀內(nèi)有多少個(gè) View 發(fā)起了刷新的請求,在這一次的遍歷過程中全部都會去處理的吧。這也是我們從代碼上看到的,每一個(gè)屏幕刷新信號來的時(shí)候,只會去執(zhí)行一次 performTraversals(),因?yàn)橹恍璞闅v一遍,就能夠刷新所有的 View 了。

而 performTraversals() 會被執(zhí)行的前提是調(diào)用了 scheduleTraversals() 來向底層注冊監(jiān)聽了下一個(gè)屏幕刷新信號事件,所以在同一個(gè) 16.6ms 的一幀內(nèi),只需要第一個(gè)發(fā)起刷新請求的 View 來走一遍 scheduleTraversals() 干的事就可以了,其他不管還有多少 View 發(fā)起了刷新請求,沒必要再去重復(fù)向底層注冊監(jiān)聽下一個(gè)屏幕刷新信號事件了,反正只要有一次遍歷繪制 View 樹的操作就可以對它們進(jìn)行刷新了。

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

還剩最后一個(gè)問題,scheduleTraversals() 里我們還有一行代碼沒分析。這個(gè)問題是這樣的:

我們清楚主線程其實(shí)是一直在處理 MessageQueue 消息隊(duì)列里的 Message,每個(gè)操作都是一個(gè) Message,打開 Activity 是一個(gè) Message,遍歷繪制 View 樹來刷新屏幕也是一個(gè) Message。

而且,上面梳理完我們也清楚,遍歷繪制 View 樹的操作是在屏幕刷新信號到的時(shí)候,底層回調(diào)我們 app 的 onVsync(),這個(gè)方法再去將遍歷繪制 View 樹的操作 post 到主線程的 MessageQueue 中去等待執(zhí)行。主線程同一時(shí)間只能處理一個(gè) Message,這些 Message 就肯定有先后的問題,那么會不會出現(xiàn)下面這種情況呢:

image

也就是說,當(dāng)我們的 app 接收到屏幕刷新信號時(shí),來不及第一時(shí)間就去執(zhí)行刷新屏幕的操作,這樣一來,即使我們將布局優(yōu)化得很徹底,保證繪制當(dāng)前 View 樹不會超過 16ms,但如果不能第一時(shí)間優(yōu)先處理繪制 View 的工作,那等 16.6 ms 過了,底層需要去切換下一幀的畫面了,我們 app 卻還沒處理完,這樣也照樣會出現(xiàn)丟幀了吧。而且這種場景是非常有可能出現(xiàn)的吧,畢竟主線程需要處理的事肯定不僅僅是刷新屏幕的事而已,那么這個(gè)問題是怎么處理的呢?

所以我們繼續(xù)回來看 scheduleTraversals():

//ViewRootImpl#scheduleTraversalsvoid scheduleTraversals() {    if (!mTraversalScheduled) {        mTraversalScheduled = true;        //1.注意這行代碼,往主線程的消息隊(duì)列里發(fā)送了一個(gè)同步屏障消息        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();        mChoreographer.postCallback(            Choreograhper.CALLBACK_TRAVERSAL, mTraversalRunnable, null);        ...    }}//ViewRootImpl#doTraversalvoid doTraversal() {    if (mTraversalScheduled) {        mTraversalScheduled = false;        //1.注意這行代碼,移除消息隊(duì)列里的同步屏障消息        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);        ...        performTraversals();        ...    }}

在邏輯走進(jìn) Choreographer 前會先往隊(duì)列里發(fā)送一個(gè)同步屏障,而當(dāng) doTraversal() 被調(diào)用時(shí)才將同步屏障移除。這個(gè)同步屏障又涉及到消息機(jī)制了,不深入了,這里就只給出結(jié)論。

這個(gè)同步屏障的作用可以理解成攔截同步消息的執(zhí)行,主線程的 Looper 會一直循環(huán)調(diào)用 MessageQueue 的 next() 來取出隊(duì)頭的 Message 執(zhí)行,當(dāng) Message 執(zhí)行完后再去取下一個(gè)。當(dāng) next() 方法在取 Message 時(shí)發(fā)現(xiàn)隊(duì)頭是一個(gè)同步屏障的消息時(shí),就會去遍歷整個(gè)隊(duì)列,只尋找設(shè)置了異步標(biāo)志的消息,如果有找到異步消息,那么就取出這個(gè)異步消息來執(zhí)行,否則就讓 next() 方法陷入阻塞狀態(tài)。如果 next() 方法陷入阻塞狀態(tài),那么主線程此時(shí)就是處于空閑狀態(tài)的,也就是沒在干任何事。所以,如果隊(duì)頭是一個(gè)同步屏障的消息的話,那么在它后面的所有同步消息就都被攔截住了,直到這個(gè)同步屏障消息被移除出隊(duì)列,否則主線程就一直不會去處理同步屏幕后面的同步消息。

而所有消息默認(rèn)都是同步消息,只有手動(dòng)設(shè)置了異步標(biāo)志,這個(gè)消息才會是異步消息。另外,同步屏障消息只能由內(nèi)部來發(fā)送,這個(gè)接口并沒有公開給我們使用。

最后,仔細(xì)看上面 Choreographer 里所有跟 message 有關(guān)的代碼,你會發(fā)現(xiàn),都手動(dòng)設(shè)置了異步消息的標(biāo)志,所以這些操作是不受到同步屏障影響的。這樣做的原因可能就是為了盡可能保證上層 app 在接收到屏幕刷新信號時(shí),可以在第一時(shí)間執(zhí)行遍歷繪制 View 樹的工作。

因?yàn)橹骶€程中如果有太多消息要執(zhí)行,而這些消息又是根據(jù)時(shí)間戳進(jìn)行排序,如果不加一個(gè)同步屏障的話,那么遍歷繪制 View 樹的工作就可能被迫延遲執(zhí)行,因?yàn)樗残枰抨?duì),那么就有可能出現(xiàn)當(dāng)一幀都快結(jié)束的時(shí)候才開始計(jì)算屏幕數(shù)據(jù),那即使這次的計(jì)算少于 16.6ms,也同樣會造成丟幀現(xiàn)象。

那么,有了同步屏障消息的控制就能保證每次一接收到屏幕刷新信號就第一時(shí)間處理遍歷繪制 View 樹的工作么?

只能說,同步屏障是盡可能去做到,但并不能保證一定可以第一時(shí)間處理。因?yàn)椋狡琳鲜窃?scheduleTraversals() 被調(diào)用時(shí)才發(fā)送到消息隊(duì)列里的,也就是說,只有當(dāng)某個(gè) View 發(fā)起了刷新請求時(shí),在這個(gè)時(shí)刻后面的同步消息才會被攔截掉。如果在 scheduleTraversals() 之前就發(fā)送到消息隊(duì)列里的工作仍然會按順序依次被取出來執(zhí)行。

總結(jié)

  1. 界面上任何一個(gè) View 的刷新請求最終都會走到 ViewRootImpl 中的 scheduleTraversals() 里來安排一次遍歷繪制 View 樹的任務(wù);

  2. scheduleTraversals() 會先過濾掉同一幀內(nèi)的重復(fù)調(diào)用,在同一幀內(nèi)只需要安排一次遍歷繪制 View 樹的任務(wù)即可,這個(gè)任務(wù)會在下一個(gè)屏幕刷新信號到來時(shí)調(diào)用 performTraversals() 遍歷 View 樹,遍歷過程中會將所有需要刷新的 View 進(jìn)行重繪;

  3. 接著 scheduleTraversals() 會往主線程的消息隊(duì)列中發(fā)送一個(gè)同步屏障,攔截這個(gè)時(shí)刻之后所有的同步消息的執(zhí)行,但不會攔截異步消息,以此來盡可能的保證當(dāng)接收到屏幕刷新信號時(shí)可以盡可能第一時(shí)間處理遍歷繪制 View 樹的工作;

  4. 發(fā)完同步屏障后 scheduleTraversals() 才會開始安排一個(gè)遍歷繪制 View 樹的操作,作法是把 performTraversals() 封裝到 Runnable 里面,然后調(diào)用 Choreographer 的 postCallback() 方法;

  5. postCallback() 方法會先將這個(gè) Runnable 任務(wù)以當(dāng)前時(shí)間戳放進(jìn)一個(gè)待執(zhí)行的隊(duì)列里,然后如果當(dāng)前是在主線程就會直接調(diào)用一個(gè)native 層方法,如果不是在主線程,會發(fā)一個(gè)最高優(yōu)先級的 message 到主線程,讓主線程第一時(shí)間調(diào)用這個(gè) native 層的方法;

  6. native 層的這個(gè)方法是用來向底層注冊監(jiān)聽下一個(gè)屏幕刷新信號,當(dāng)下一個(gè)屏幕刷新信號發(fā)出時(shí),底層就會回調(diào) Choreographer 的onVsync() 方法來通知上層 app;

  7. onVsync() 方法被回調(diào)時(shí),會往主線程的消息隊(duì)列中發(fā)送一個(gè)執(zhí)行 doFrame() 方法的消息,這個(gè)消息是異步消息,所以不會被同步屏障攔截住;

  8. doFrame() 方法會去取出之前放進(jìn)待執(zhí)行隊(duì)列里的任務(wù)來執(zhí)行,取出來的這個(gè)任務(wù)實(shí)際上是 ViewRootImpl 的 doTraversal() 操作;

  9. 上述第4步到第8步涉及到的消息都手動(dòng)設(shè)置成了異步消息,所以不會受到同步屏障的攔截;

  10. doTraversal() 方法會先移除主線程的同步屏障,然后調(diào)用 performTraversals() 開始根據(jù)當(dāng)前狀態(tài)判斷是否需要執(zhí)行performMeasure() 測量、perfromLayout() 布局、performDraw() 繪制流程,在這幾個(gè)流程中都會去遍歷 View 樹來刷新需要更新的View;

再來一張時(shí)序圖結(jié)尾,大伙想自己過源碼時(shí)可以跟著時(shí)序圖來,建議在電腦上閱讀:

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容