"一文讀懂"系列:Android螢幕重新整理機制

語言: CN / TW / HK

本文正在參加「金石計劃 . 瓜分6萬現金大獎」

🔥 Hi,我是小余。本文已收錄到 GitHub · Androider-Planet 中。這裡有 Android 進階成長知識體系,關注公眾號 [小余的自習室] ,在成功的路上不迷路!

為什麼要學習螢幕重新整理知識?

很多同學覺得螢幕重新整理繪製知識點對他們開發不重要,沒必要學習這些東西,這部分同學可能平時維護的是一些中小型專案或者應用是安裝在特定裝置上,只要求寫寫主介面,做一些簡單的網路請求,業務互動相關知識,對效能這塊要求不是很高,確實涉及不到太多螢幕重新整理這塊知識。

又不是不能用.jpg

但對一些大中型專案來說可能就不一樣了:他們涉及業務較多,裝置種類較多,往往一個app內部集成了十幾個子業務甚至上百個,這對應用效能要求就更加嚴格了,app的體驗也會間接導致使用者的留存問題

所以學習螢幕繪製這類理論性較強的知識也是非常有必要的。

如果你想進階成為高階開發:螢幕繪製這塊知識也是一個繞不過去的坎。

1.帶著問題出發

前面一篇卡頓優化的文章我們說過,主流螢幕重新整理頻率是每秒60次(高的有90,120等),也就是16.6ms重新整理一次螢幕, 如果我們在主執行緒做一些耗時的操作,最直觀的現象就是螢幕卡頓,其實就是發生了丟幀。

由此丟擲幾個問題: - 1.16.6ms是什麼意思,每次16.6ms都會呼叫一個繪製流程麼? - 2.為什麼在主執行緒做一些耗時操作會出現卡頓?丟幀? - 3.丟幀是個什麼意思,是字面上的直接丟棄當前幀還是延後顯示當前幀? - 4.雙緩衝是什麼?有了雙快取就不會出現丟幀了麼?三緩衝呢? - 5.瞭解Vsync麼?它的作用是什麼? - 6.畫面撕裂是怎麼造成的? - 7.編舞者是個什麼東西?

帶著這些問題我們出發吧。

2.Android螢幕重新整理前置知識

CPU/GPU:

  • CPU中央處理器,主要用於計算資料,在Android中主要用於三大繪製流程中Surface的計算過程,起著生產者的作用
  • GPU影象處理器,主要用於遊戲畫面的渲染,在Android中主要用於將CPU計算好的Surface資料合成後放到buffer中,讓顯示器進行讀取,起著消費者的作用。

如下圖:

螢幕重新整理過程.gif 其中GPU在架構中是以SurfaceFlinger服務的形式工作

SurfaceFlinger:

SurfaceFlinger作用是接受多個來源的圖形顯示資料Surface,合成後傳送到顯示裝置。

比如我們的主介面中:可能會有statusBar,側滑選單,主介面,這些View都是獨立Surface渲染和更新,最後提交給SF後,SF根據Zorder,透明度,大小,位置等引數,合成為一個數據buffer,傳遞HWComposer或者OpenGL處理,最終給顯示器

cpu和gpu.webp

逐行掃描

螢幕在重新整理buffer的時候,並不是一次性掃描完成,而是從左到右,從上到下的一個讀取過程,順序顯示一屏的每個畫素點,你為什麼看不到,因為太快了嘛,按60HZ的螢幕重新整理率來算,這個過程只有16.66666...ms。

幀、幀率(數)、螢幕重新整理率:

在影片領域中,幀就代表一張圖片。玩過短影片剪輯的朋友應該對此很瞭解。

圖中為放大後的一幀圖片

幀截圖.jpg

而幀率和螢幕重新整理率確是兩個不同的概念: - 幀率:表示GPU在1s中內可以渲染多少幀到buffer中,單位是fps,這裡要理解的是幀率是一個動態的,比如我們平時說的60fps,只是1s內最多可以渲染60幀,假如我們螢幕是靜止的,則GPU此時就沒有任何操作,幀率就為0. - 螢幕重新整理率:螢幕在1s內去buffer中取資料的次數,單位為HZ,常見螢幕重新整理率為60HZ。和幀率不一樣,螢幕重新整理率是一個固定值和硬體引數有關。

畫面撕裂:

畫面撕裂簡單說就是顯示器把多個幀顯示在同一個畫面中。如圖:

螢幕撕裂.webp

畫面撕裂的原因:我們知道螢幕重新整理率是固定的,假設為60HZ,正常情況下當我們的GPU的幀率也是60fps的時候,GPU繪製完一幀,螢幕重新整理一幀,這樣是不會出問題的,但是隨著GPU顯示卡效能的提升,GPU的幀率超過60fps後,就會出現畫面撕裂的情況,實際在幀率較低的時候也會出現撕裂的情況。

所以其本質是幀率和螢幕重新整理率的不一致導致的撕裂

那可能大家要說了,等螢幕一幀重新整理完成後,再將新的一幀存到buffer中不就可以了,那你要知道,早期的4.0之前裝置是隻有一個buffer,且其並沒有buffer同步的概念,螢幕讀取buffer中的資料時,GPU是不知道的,螢幕讀取的同時,GPU也在寫入,導致buffer被覆蓋,出現同一畫面使用的是不同幀的資料

那既然是因為使用同一個Buffer引起的畫面撕裂,使用兩個buffer不就可以了

雙緩衝

前面我們說到畫面撕裂是由於單buffer引起的,在4.1之前,使用了雙緩衝來解決畫面撕裂。

  • GPU寫入的快取為:Back Buffer
  • 螢幕重新整理使用的快取為:Frame Buffer

如下圖:

雙buffer.png 因為使用雙buffer,螢幕重新整理時,frame buffer不會發生變化,通過交換buffer來實現幀資料切換,那什麼時候交換buffer呢?

這就要引入Vsync的概念了。

VSync(垂直同步)

我們知道如果一個螢幕在重新整理的過程中,是不能交換buffer的,只有等螢幕重新整理完成後以後才可以考慮buffer的交換.

那具體什麼時候交換呢當裝置螢幕重新整理完畢後到下一幀重新整理前,因為沒有螢幕重新整理,所以這段時間就是快取交換的最佳時間

此時硬體螢幕會發出一個脈衝訊號,告知GPU和CPU可以交換了,這個就是Vsync訊號。

有了雙緩衝和VSync是不是就都ok了?雖然上面方式可以解決螢幕撕裂的問題,但是還是會出現一些其他問題。

Jank

雙緩衝buffer交換還有個前提就是GPU已經準備好了back buffer的數據,如果在Vsync到來時back buffer並沒有準備好,就不會進行快取的交換,螢幕顯示的還是前一幀畫面,這種情況就是Jank。

有了上面的基礎我們再來聊聊Android螢幕重新整理機制的演變過程

3.螢幕重新整理機制的演變過程

Android螢幕重新整理機制演變過程按buffer的個數可以分為3個階段: - 1.單buffer時代 - 2.雙buffer時代 - 3.三buffer時代

1.單buffer時代

GPU和顯示器共用一塊buffer,會引起畫面撕裂。

2.雙buffer時代

2.1:在引入VSync前(Drawing without VSync)

drawing without vsync.png - CPU:表示CPU繪製的時間段 - GPU:表示GPU合成back buffer的時間段 - Display:顯示器讀取frame buffer的時間段

按時間順序: - 1.Display顯示第0幀畫面,而CPU和GPU正在合成第1幀,且在Display顯示下一幀之前完成了。 - 2.由於GPU在Display第一個VSync來之前完成了back buffer的填充,此時交換back buffer和frame buffer,螢幕進行重新整理,可以正常顯示第一幀資料。 - 3.再來看第2個VSync,第二個VSync到來之時,GPU並沒有及時的填充back buffer,這個時候不能互動buffer,螢幕重新整理的還是第1幀的畫面。就說這裡發生了“jank” - 4.在第3個VSync訊號到來時,第2幀的資料已經寫入back buffer,第3幀的資料GPU還沒寫入,所以這個時候互動buffer顯示的是第2幀的資料 - 5.同理,在第4個VSync時,第3幀資料已經處理完畢,交換buffer後顯示的是第2幀的資料

這裡發生jank的原因是:在第2幀CPU處理資料的時候太晚了,GPU沒有及時將資料寫入到buffer中,導致jank的發生。

如果可以把CPU繪製流程提前到每個VSync訊號來的時候進行CPU的繪製,那是不是就可以讓CPU的計算以及GPU的合成寫入buffer的操作有完整的16.6ms。

2.1:在引入VSync後(Drawing with VSync)

為了進一步優化效能,谷歌在4.1之後對螢幕繪製與重新整理過程引入了Project Butter黃油工程),系統在收到VSync訊號之後,馬上進行CPU的繪製以及GPU的buffer寫入。 這樣就可以讓cpu和gpu有個完整的16.6ms處理過程。最大限度的減少jank的發生。

drawing withvsync.png

引入VSync後,新的問題又出現了:如下圖:

with問題.png

由於主執行緒做了一些相對複雜耗時邏輯,導致CPU和GPU的處理時間超過16.6ms,由於此時back buffer寫入的是B幀資料,在交換buffer前不能被覆蓋,而frame buffer被Display用來做重新整理用,所以在B幀寫入back buffer完成到下一個VSync訊號到來之前兩個buffer都被佔用了,CPU無法繼續繪製,這段時間就會被空著, 於是又出現了三快取。

3.三buffer時代

為了進一步優化使用者體驗,Google在雙buffer的基礎上又增加了第三個buffer(Graphic Buffer), 如圖:

三buffer.png 按時間順序: - 1.第一個jank是無法避免的,因為第一個B幀處理超時,A幀肯定是會重複的。 - 2.在第一個VSync訊號來時,雖然back buffer以及frame buffer都被佔用了,CPU此時會啟用第三個Graphic Buffer,避免了CPU的空閒狀態。

這裡可以最大限度避免2中CPU空閒的情況,記住只是最大限度,沒有說一定能避免。

那又有人要說了,那就再多開幾個不就可以了,是的,buffer越多jank越少,但是你得考慮性價比: 3 buffer已經可以最大限度的避免jank的發生了,再多的buffer起到的作用就微乎其微,反而因為buffer的數量太多,浪費更多記憶體,得不償失。 buffer收益比:

不過假想下哪天由於硬體的改進,3 buffer已經滿足不了的時候,谷歌又會加4 buffer,5 buffer..這都是可能的事情。

4.Choreographer

前面我們分析“雙buffer時代“說過:谷歌在4.1之後對螢幕繪製與重新整理過程引入了Project Butter(黃油工程),系統在收到VSync訊號之後,馬上進行CPU的繪製以及GPU的buffer寫入。這樣就可以讓cpu和gpu有個完整的16.6ms處理過程。最大限度的減少jank的發生。

那麼在Android原始碼層是如何實現的呢?

那就是這小節要講解的Choreographer,譯為編舞者,多麼唯美的詞,看來寫這個原始碼的開發者也是個很優雅的紳士。

Choreographer在螢幕繪製中的作用: - 1.註冊VSync訊號回撥 - 2.接收SurfaceFlinger服務回撥的onSync事件,SurfaceFlinger服務在接收到硬體發出的定時中斷訊號VSync後,將訊號傳遞給App,這裡App的接收者就是1中註冊的回撥。 一般SurfaceFlinger服務接收到的中斷訊號VSync和App收到的VSync回撥是有個offsets的。

下面就來看下Choreographer在原始碼層的工作原理:

首先聲明當前使用的是8.0的原始碼

1.原始碼入口scheduleTraversals:

我們知道一個View在新增到視窗中時,繪製流程會呼叫到ViewRootImpl的setView()方法,setView方法會呼叫requestLayout()方法請求繪製,requestLayout方法中會呼叫scheduleTraversals()方法,那就從scheduleTraversals開始吧。

```java void scheduleTraversals() { //這個欄位保證該View已經繪製過,不會重複繪製 if (!mTraversalScheduled) { mTraversalScheduled = true; //添加了一個同步屏障,保證非同步繪製訊息優先執行 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); //post一個繪製的Runnable任務,即View的layout/measure/draw流程以及後續的GPU合成流程。 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

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

void doTraversal() {
    //已經繪製過,才會走到內部
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
        //開始執行performTraversals
        performTraversals();

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

```

scheduleTraversals方法主要做了下面事情:
- 1.先檢查mTraversalScheduled是否已經繪製過,沒有繪製過繼續走下面流程,並將mTraversalScheduled標誌置為true,防止重複繪製 - 2.呼叫當前Handler的Looper的MessageQueue的postSyncBarrier,設定一個同步屏障。 - 3.使用mChoreographer傳送一個Choreographer.CALLBACK_TRAVERSAL的任務。

進入mChoreographer.postCallback方法裡面看看:

2.任務提交postCallback

postCallback最終會呼叫到postCallbackDelayedInternal方法.

```java private void postCallbackDelayedInternal(int callbackType,..) { ... synchronized (mLock) { final long now = SystemClock.uptimeMillis(); final long dueTime = now + delayMillis; //將action封裝到一個CallBackRecord中並放到mCallbackQueues的callbackType索引處 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
    //表示delayMillis=0 立即執行
    scheduleFrameLocked(now);
} else {
    //傳送一個非同步延遲任務msg.what = MSG_DO_SCHEDULE_CALLBACK,action = mTraversalRunnable,
    //這裡經過Handler後最終也會執行到scheduleFrameLocked
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
    msg.arg1 = callbackType;
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, dueTime);
}

} } ``` postCallbackDelayedInternal做了下面這些事情:

  • 1.將action封裝到一個CallBackRecord中並放到mCallbackQueues的callbackType索引處
  • 2.如果是立即執行的訊息,則直接呼叫scheduleFrameLocked
  • 3.如果是延遲訊息,則傳送一個MSG_DO_SCHEDULE_CALLBACK的msg

我們來看下MSG_DO_SCHEDULE_CALLBACK的Handler邏輯:

這裡mHandler = FrameHandler類物件

```java 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(System.nanoTime(), 0); break; case MSG_DO_SCHEDULE_VSYNC: doScheduleVsync(); break; case MSG_DO_SCHEDULE_CALLBACK: doScheduleCallback(msg.arg1); break; } } ```

MSG_DO_SCHEDULE_CALLBACK的訊息型別會走到doScheduleCallback,msg.arg1 = callbackType,

進入doScheduleCallback方法:

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

判斷mCallbackQueues[callbackType]是否有需要任務。有任務則執行scheduleFrameLocked
可以看到postCallbackDelayedInternal最終都是執行scheduleFrameLocked方法

直接看scheduleFrameLocked方法

3.回撥註冊scheduleVsync

```java private void scheduleFrameLocked(long now) { //檢測mFrameScheduled是否已經為true if (!mFrameScheduled) { mFrameScheduled = true; //是否開啟了VSYNC,4.1之後預設開啟,直接看這裡即可 if (USE_VSYNC) { if (DEBUG_FRAMES) { Log.d(TAG, "Scheduling next frame on vsync."); }

                // If running on the Looper thread, then schedule the vsync immediately,
                // otherwise post a message to schedule the vsync from the UI thread
                // as soon as possible.
                //如果是在主執行緒上,則直接呼叫scheduleVsyncLocked
                if (isRunningOnLooperThreadLocked()) {
                        scheduleVsyncLocked();
                } else {
                        //其他執行緒則需要使用Handler將任務執行在主執行緒中。
                        Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                        msg.setAsynchronous(true);
                        mHandler.sendMessageAtFrontOfQueue(msg);
                }
        } else {
                final long nextFrameTime = Math.max(
                                mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
                if (DEBUG_FRAMES) {
                        Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
                }
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
        }
}

} ```

  • 1.檢測mFrameScheduled是否已經為true
  • 2.如果開啟了VSYNC則呼叫scheduleVsyncLocked方法,沒有開啟則傳送一個MSG_DO_FRAME的msg給mHandler。Android 4.1之後預設開啟了VSYNC,所以直接看USE_VSYNC流程即可
  • 3.如果是執行在當前執行緒的上,當前執行緒是UI執行緒。則直接呼叫scheduleVsyncLocked方法。
  • 4.如果沒有執行在主執行緒上, 則傳送一個MSG_DO_SCHEDULE_VSYNC的msg給mHandler。根據FrameHandler的原始碼可以看出最終也是走到scheduleVsyncLocked方法

看scheduleVsyncLocked方法:

```java private void scheduleVsyncLocked() { mDisplayEventReceiver.scheduleVsync(); } 這個mDisplayEventReceiver是什麼時候賦值的呢。 我們來看Choreographer的構造方法: private Choreographer(Looper looper, int vsyncSource) { //設定looper mLooper = looper; //建立mHandler例項 mHandler = new FrameHandler(looper); //4.1預設開啟,所以直接使用的是FrameDisplayEventReceiver類物件 mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper, vsyncSource) : null; mLastFrameTimeNanos = Long.MIN_VALUE;

mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

//初始化一個CallbackQueue陣列物件
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
    mCallbackQueues[i] = new CallbackQueue();
}

} ``` 回到scheduleVsyncLocked方法:呼叫了mDisplayEventReceiver.scheduleVsync(),mDisplayEventReceiver是FrameDisplayEventReceiver類物件

進入FrameDisplayEventReceiver類scheduleVsync方法中,子類未實現,在其父類DisplayEventReceiver中實現

```java 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); } }

public DisplayEventReceiver(Looper looper, int vsyncSource) {
if (looper == null) {
    throw new IllegalArgumentException("looper must not be null");
}

mMessageQueue = looper.getQueue();
//這個方法會將當前物件this = mDisplayEventReceiver以及mMessageQueue,vsyncSource傳遞給native層物件,並返回native層物件的地址值mReceiverPtr
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
        vsyncSource);

mCloseGuard.open("dispose");

} ```

scheduleVsync中呼叫的是nativeScheduleVsync方法進行註冊,註冊的是一個mReceiverPtr這是一個native層的物件地址,這個地址是在DisplayEventReceiver構造方法中初始化,呼叫nativeInit方法返回的nativeInit方法傳入一個this,這個this就是前面的mDisplayEventReceiver物件,所以重新回到前面的mDisplayEventReceiver講解,

```java 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) {
                ...           
    mFrame = frame;
    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類實現了onVsync方法,這個方法就是native層在接收到VSync訊號後回撥的方法。onVsync方法直接傳送一個非同步訊息,執行的任務是自己的run方法

run中執行doFrame(),進入doFrame看看:

4.圖幀繪製doFrame

```java void doFrame(long frameTimeNanos, int frame) { ... try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame"); AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

    mFrameInfo.markInputHandlingStart();
                //處理輸入事件
    doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

    mFrameInfo.markAnimationsStart();
                //處理動畫事件
    doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

    mFrameInfo.markPerformTraversalsStart();
                //處理CALLBACK_TRAVERSAL,三大繪製流程
    doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
                //處理CALLBACK_COMMIT事件
    doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
    AnimationUtils.unlockAnimationClock();
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
...

} ```

doFrame中: - 1.執行輸入事件 - 2.處理動畫事件 - 3.處理CALLBACK_TRAVERSAL,三大繪製流程,其實就是前面的mTraversalRunnable事件 - 4.處理CALLBACK_COMMIT提交幀事件

進入doCallbacks方法:

java void doCallbacks(int callbackType, long frameTimeNanos) { for (CallbackRecord c = callbacks; c != null; c = c.next) { if (DEBUG_FRAMES) { Log.d(TAG, "RunCallback: type=" + callbackType + ", action=" + c.action + ", token=" + c.token + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime)); } c.run(frameTimeNanos); } } 迴圈執行callbacks中的記錄:

```java private static final class CallbackRecord { public CallbackRecord next; public long dueTime; public Object action; // Runnable or FrameCallback public Object token;

public void run(long frameTimeNanos) {
    if (token == FRAME_CALLBACK_TOKEN) {
        ((FrameCallback)action).doFrame(frameTimeNanos);
    } else {
        ((Runnable)action).run();
    }
}

} ```

CallbackRecord的run方法在token = null的情況下執行的是action的run方法 這裡再看

java mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 傳入的token確實為null,且action = mTraversalRunnable

這樣整個處理流程就閉環了。

這裡token = FRAME_CALLBACK_TOKEN是在什麼情況下呢?

5.幀率計算postFrameCallback

在Choreographer的postFrameCallback方法中:

```java public void postFrameCallback(FrameCallback callback) { postFrameCallbackDelayed(callback, 0); }

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { if (callback == null) { throw new IllegalArgumentException("callback must not be null"); }

postCallbackDelayedInternal(CALLBACK_ANIMATION,
        callback, FRAME_CALLBACK_TOKEN, delayMillis);

} ```

最終也是執行到postCallbackDelayedInternal方法,不同之處在於,其傳入的token是FRAME_CALLBACK_TOKEN

那麼這個方法有什麼作用呢?計算丟幀

我們使用下面的方法對丟丟幀30次以上在logcat中列印一個日誌。

  • 1.建立一個FrameCallback子類

```java public class YourFrameCallback implements Choreographer.FrameCallback {

private static final String TAG = "FPS_TEST"; private long mLastFrameTimeNanos = 0; private long mFrameIntervalNanos;

public YourFrameCallback(long lastFrameTimeNanos) { mLastFrameTimeNanos = lastFrameTimeNanos; mFrameIntervalNanos = (long)(1000000000 / 60.0); }

@Override public void doFrame(long frameTimeNanos) {

  //初始化時間
  if (mLastFrameTimeNanos == 0) {
      mLastFrameTimeNanos = frameTimeNanos;
  }
  final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
  if (jitterNanos >= mFrameIntervalNanos) {
      final long skippedFrames = jitterNanos / mFrameIntervalNanos;
      if(skippedFrames>30){
          //丟幀30以上logcat列印日誌
          Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                  + "The application may be doing too much work on its main thread.");
      }
  }
  mLastFrameTimeNanos=frameTimeNanos;
  //因為每次doFrame都會消費掉,需要重新註冊下一幀回撥
  Choreographer.getInstance().postFrameCallback(this);

} } ``` - 2.在Application啟動的時候:呼叫Choreographer的postFrameCallback方法,並傳入一個FrameCallback

java Choreographer.getInstance().postFrameCallback(new YourFrameCallback(System.nanoTime()));

對這小節總結: - 1.在Choreographer的建構函式中會建立一個FrameDisplayEventReceiver類物件,這個物件實現了onVSync方法,用於VSync訊號回撥。 - 2.FrameDisplayEventReceiver這個物件的父類構造方法中會呼叫nativeInit方法將當前FrameDisplayEventReceiver物件傳遞給native層,native層返回一個地址mReceiverPtr給上層。 - 3.主執行緒在scheduleVsync方法中呼叫nativeScheduleVsync,並傳入2中返回的mReceiverPtr,這樣就在native層就正式註冊了一個FrameDisplayEventReceiver物件。 - 4.native層在GPU的驅使下會定時回撥FrameDisplayEventReceiver的onVSync方法,從而實現了:在VSync訊號到來時,立即執行doFrame方法 - 5.doFrame方法中會執行輸入事件,動畫事件,layout/measure/draw流程並提交資料給GPU。這樣就閉環了

繪製流程圖如下:來自《Android 之 Choreographer 詳細分析

Android 之 Choreographer 詳細分析.png

5.Handler同步屏障機制

核心思想:在主執行緒Looper獲取msg的時候,讓非同步訊息優先執行,同步訊息滯後

這裡先上一張原理圖:

同步機制.png 我們依次對圖中幾個點進行原始碼講解: - 1.同步屏障的建立 - 2.非同步訊息的優先執行

1.同步屏障的建立

使用的是:MessageQueue#postSyncBarrier()

```java /* * 同步屏障就是一個同步訊息,只不過這個訊息的target為null / private int postSyncBarrier(long when) { // Enqueue a new sync barrier token. // We don't need to wake the queue because the purpose of a barrier is to stall it. synchronized (this) { final int token = mNextBarrierToken++; // 從訊息池中獲取Message final Message msg = Message.obtain(); msg.markInUse(); // 初始化Message物件的時候,並沒有給Message.target賦值, // 因此Message.target==null msg.when = when; msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            // 這裡的when是要加入的Message的時間
            // 這裡遍歷是找到Message要加入的位置
            while (p != null && p.when <= when) {
                    // 如果開啟同步屏障的時間(假設記為T)T不為0,且當前的同步
                    // 訊息裡有時間小於T,則prev也不為null
                    prev = p;
                    p = p.next;
            }
        }
        // 根據prev是否為null,將msg按照時間順序插入到訊息佇列的合適位置
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }

} ```

註釋中有說明了,這裡的屏障訊息就是一個target為null的訊息,因為其不需要執行任務訊息,只是用來設定一堵牆.

再根據時間戳將其插入到合適的位置。

2.非同步訊息的優先執行

移步到:MessageQueue的next方法:

```java Message next() { ...

int pendingIdleHandlerCount = -1; // -1 only during first iteration
// 1.如果nextPollTimeoutMillis=-1,一直阻塞不會超時
// 2.如果nextPollTimeoutMillis=0,不會阻塞,立即返回
// 3.如果nextPollTimeoutMillis>0,最長阻塞nextPollTimeoutMillis毫秒(超時)
int nextPollTimeoutMillis = 0;
for (;;) {
    if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
    }

    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
        // 獲取系統開機到現在的時間戳
        final long now = SystemClock.uptimeMillis();
        Message prevMsg = null;
        Message msg = mMessages;
        // 取出target==null的訊息
        // 如果target==null,那麼它就是屏障,需要迴圈遍歷,
        // 一直往後找到第一個非同步訊息,即msg.isAsynchronous()為true
        // 這個target==null的訊息,不會被取出處理,一直會存在
        // 每次處理非同步訊息的時候,都會從頭開始輪詢
        // 都需要經歷從msg.target開始的遍歷
        if (msg != null && msg.target == null) {
                // 使用一個do..while迴圈
                // 輪詢訊息佇列裡的訊息,這裡使用do..while迴圈的原因
                // 是因為do..while迴圈中取出的這第一個訊息是target==null的訊息
                // 這個訊息是同步屏障的標誌訊息
                // 接下去進行遍歷迴圈取出Message.isAsynchronous()為true的訊息
                // isAsynchronous()為true就是非同步訊息
                do {
                        prevMsg = msg;
                        msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
        }
        if (msg != null) {
                // 如果有訊息需要處理,先判斷時間有沒有到,如果沒有到的話設定阻塞時間
                if (now < msg.when) {
                        // 計算出離執行時間還有多久賦值給nextPollTimeoutMillis
                        // 表示nativePollOnce方法要等待nextPollTimeoutMillis時長後返回
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                        // 獲取到訊息
                        mBlocked = false;
                        // 連結串列操作,獲取msg並且刪除該節點
                        if (prevMsg != null) {
                                prevMsg.next = msg.next;
                        } else {
                                mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        // 返回拿到的訊息
                        return msg;
                }
        } else {
                // 沒有訊息,nextPollTimeoutMillis復位
                nextPollTimeoutMillis = -1;
        }

            ...
    }
}

} ```

優先判斷是否有同步屏障存在,然後取同步屏障後面的非同步訊息進行處理。就達到了優先執行非同步訊息的目的。

好了,關於同步屏障機制就講到這裡。

有了上面這些基礎講解。下面對一開始的問題進行一個總結歸納。

6.問題總結

1.16.6ms是什麼意思,每次16.6ms都會呼叫一個繪製流程麼?

16.6ms是指重新整理頻率為是60HZ,1s需要執行60次,平均每次16.6ms。也可以理解為VSync的一個週期是16.6ms。 並非每次16.6ms都會執行三大繪製流程,螢幕靜止狀態,CPU並不會執行繪製流程

2.畫面撕裂是怎麼造成的?

畫面撕裂是早期使用的是一個buffer進行螢幕的重新整理讀取和GPU的寫入操作,且不存在同步鎖的情況下,新資料覆蓋舊的資料導致一張畫面顯示多幀的場景

3.為什麼在主執行緒耗時,佈局層級太多,會出現卡頓?丟幀?

主執行緒耗時,佈局層級太多會影響CPU的計算和GPU的合成過程,超過VSync訊號後,需要等下一個VSync訊號來才能進行buffer交換,發生丟幀現象

4.丟幀是個什麼意思,是字面上的直接丟棄當前幀還是延後顯示當前幀?

丟幀是指在第一個VSync訊號來之前並沒有合成好back buffer資料,無法交換buffer,螢幕重新整理的還是上一幀資料,這就是丟幀。 下一個VSync訊號來之後,back buffer繪製好後,再交換buffer,所以其不會丟棄,而是延後顯示,直觀感受就是卡頓。

5.雙緩衝是什麼?有了雙快取就萬事大吉了麼?三緩衝呢?

雙緩衝是指使用兩個buffer進行資料的快取,一個用於GPU的合成,一個用於螢幕的重新整理,互不干擾,防止出現畫面撕裂的場景 有了雙快取還是會出現丟幀的現象和CPU的空閒等問題。 三緩衝就是在雙緩衝上再增加一個Graphic Buffer,避免CPU長時間空閒。

6.瞭解Vsync麼?它的作用是什麼?

VSync(垂直同步)有兩個作用: 1.提醒GPU進行buffer的交換 2.提醒CPU立即進入螢幕繪製過程,別閒著啦。

7.編舞者是個什麼東西?

編舞者Choreographer是4.1以後引入的: 作用:用來提醒CPU在VSync訊號來時立即對View進行繪製,防止出現CPU空閒狀態。

好了,就講到這裡吧,上面文章涉及了:UI繪製流程,Handler同步非同步訊息機制,螢幕重新整理流程,卡頓優化等知識點

如果你能把本文涉及的知識點都吸收,對後期framework層的學習是很有幫助的。

如果你喜歡我的文章,請幫忙點贊,關注,這是對我的巨大鼓勵!

歡迎關注我的公眾號小余的自習室

參考

資料的流動——計算機是如何顯示一個畫素的

聊聊Android螢幕重新整理機制

Android垂直同步訊號VSync的產生及傳播結構詳解