【面試專題】Android屏幕刷新機制

這個問題在其他人整理的面試寶典中也有提及,一般來說都是問View的刷新,基本上從ViewRootImpl的scheduleTraversals()方法開始講就可以了。之前看別人面試斗魚的面經(jīng),被問到了Android屏幕刷新機制、雙緩沖、三緩沖、黃油計劃,然后我面網(wǎng)易云的時候也確實被問到了這個題目。

屏幕刷新這一整套,你把我這篇文章里的內容講清楚了,肯定ok了。網(wǎng)易云還附加問了我CPU和GPU怎么交換繪制數(shù)據(jù)的,這個我個人認為完全是加分題了,我答不出來,感興趣的小伙伴可以去看一看,你要是能說清楚,肯定能讓面試官眼前一亮。

雙緩沖

在講雙緩沖這個概念之前,先來了解一些基礎知識。

顯示系統(tǒng)基礎

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

  • 畫面撕裂

屏幕刷新頻是固定的,比如每16.6ms從buffer取數(shù)據(jù)顯示完一幀,理想情況下幀率和刷新頻率保持一致,即每繪制完成一 幀,顯示器顯示一幀。但是CPU/GPU寫數(shù)據(jù)是不可控的,所以會出現(xiàn)buffer里有些數(shù)據(jù)根本沒顯示出來就被重寫了,即 buffer里的數(shù)據(jù)可能是來自不同的幀的。當屏幕刷新時,此時它并不知道buffer的狀態(tài),因此從buffer抓取的幀并不是完整的一幀畫面,即出現(xiàn)畫面撕裂。

簡單說就是Display在顯示的過程中,buffer內數(shù)據(jù)被CPU/GPU修改,導致畫面撕裂。

那咋解決畫面撕裂呢? 答案是使用雙緩沖。

雙緩沖

由于圖像繪制和屏幕讀取 使用的是同個buffer,所以屏幕刷新時可能讀取到的是不完整的一幀畫面。

雙緩沖,讓繪制和顯示器擁有各自的buffer:GPU 始終將完成的一幀圖像數(shù)據(jù)寫入到 Back Buffer,而顯示器使用 Frame Buffer,當屏幕刷新時,F(xiàn)rame Buffer 并不會發(fā)生變化,當Back buffer準備就緒后,它們才進行交換。

VSync

什么時候進行兩個buffer的交換呢?

假如是 Back buffer準備完成一幀數(shù)據(jù)以后就進行,那么如果此時屏幕還沒有完整顯示上一幀內容的話,肯定是會出問題的。 看來只能是等到屏幕處理完一幀數(shù)據(jù)后,才可以執(zhí)行這一操作了。

當掃描完一個屏幕后,設備需要重新回到第一行以進入下一次的循環(huán),此時有一段時間空隙,稱為VerticalBlanking Interval(VBI)。這個時間點就是我們進行緩沖區(qū)交換的最佳時間。因為此時屏幕沒有在刷新,也就避免了交換過程中出現(xiàn)畫面撕裂的狀況。

VSync(垂直同步)是VerticalSynchronization的簡寫,它利用VBI時期出現(xiàn)的vertical sync pulse(垂直同步脈沖)來保證雙緩沖在最佳時間點才進行交換。另外,交換是指各自的內存地址,可以認為該操作是瞬間完成。

所以說VSync這個概念并不是Google首創(chuàng)的,它在早年的PC機領域就已經(jīng)出現(xiàn)了。

Android屏幕刷新機制

先總體概括一下,Android屏幕刷新使用的是“雙緩存+VSync機制”,單純的雙緩沖模式容易造成jank(丟幀)現(xiàn)象,為了解決這個問題,Google在 Android4.1 提出了Project Butter(?油工程),引入了 drawing with VSync 的概念。

jank(丟幀)

VSync.jpeg

以時間的順序來看下將會發(fā)生的過程:

  1. Display顯示第0幀數(shù)據(jù),此時CPU和GPU渲染第1幀畫面,且在Display顯示下一幀前完成
  2. 因為渲染及時,Display在第0幀顯示完成后,也就是第1個VSync后,緩存進行交換,然后正常顯示第1幀
  3. 接著第2幀開始處理,是直到第2個VSync快來前才開始處理的。
  4. 第2個VSync來時,由于第2幀數(shù)據(jù)還沒有準備就緒,緩存沒有交換,顯示的還是第1幀。這種情況被Android開發(fā)組命名為“Jank”,即發(fā)生了丟幀。
  5. 當?shù)?幀數(shù)據(jù)準備完成后,它并不會?上被顯示,而是要等待下一個VSync 進行緩存交換再顯示。

所以總的來說,就是屏幕平白無故地多顯示了一次第1幀。 原因是第2幀的CPU/GPU計算 沒能在VSync信號到來前完成。

這里注意一下一個細節(jié),jank(丟幀、掉幀),不是說這一幀丟棄了不顯示,而是這一幀延遲顯示了,因為緩存交換的時機只能等下一個VSync了。

黃油計劃 —— drawing with VSync

為了優(yōu)化顯示性能,Google在Android 4.1系統(tǒng)中對Android Display系統(tǒng)進行了重構,實現(xiàn)了Project Butter(?油工程): 系統(tǒng)在收到VSync pulse后,將?上開始下一幀的渲染。即一旦收到VSync通知(16ms觸發(fā)一次),CPU和GPU 才立刻開 始計算然后把數(shù)據(jù)寫入buffer。如下圖:

VSync2.jpeg

CPU/GPU根據(jù)VSYNC信號同步處理數(shù)據(jù),可以讓CPU/GPU有完整的16ms時間來處理數(shù)據(jù),減少了jank。 一句話總結,VSync同步使得CPU/GPU充分利用了16.6ms時間,減少jank。

問題又來了,如果界面比較復雜,CPU/GPU的處理時間較?,超過了16.6ms呢?如下圖:

VSync3.jpeg
  1. 在第二個時間段內,但卻因 GPU 還在處理 B 幀,緩存沒能交換,導致 A 幀被重復顯示。
  2. 而B完成后,又因為缺乏VSync pulse信號,它只能等待下一個signal的來臨。于是在這一過程中,有一大段時間是被浪費的。
  3. 當下一個VSync出現(xiàn)時,CPU/GPU?上執(zhí)行操作(A幀),且緩存交換,相應的顯示屏對應的就是B。這時看起來就是正常的。只不過由于執(zhí)行時間仍然超過16ms,導致下一次應該執(zhí)行的緩沖區(qū)交換又被推遲了——如此循環(huán)反復,便出現(xiàn)了越來越多的“Jank”。

為什么 CPU 不能在第二個 16ms 處理繪制工作呢? 因為只有兩個 buffer,Back buffer正在被GPU用來處理B幀的數(shù)據(jù), Frame buffer的內容用于Display的顯示,這樣兩個 buffer都被占用,CPU 則無法準備下一幀的數(shù)據(jù)。 那么,如果再提供一個buffer,CPU、GPU 和顯示設備都能使用各自的 buffer工作,互不影響。這就是三緩沖的來源了。

三緩沖

三緩存就是在雙緩沖機制基礎上增加了一個 Graphic Buffer 緩沖區(qū),這樣可以最大限度的利用空閑時間,帶來的壞處是多使用的一個 Graphic Buffer 所占用的內存。

VSync4.jpeg
  1. 第一個Jank,是不可避免的。但是在第二個 16ms 時間段,CPU/GPU 使用 第三個 Buffer 完成C幀的計算,雖然還是 會多顯示一次 A 幀,但后續(xù)顯示就比較順暢了,有效避免 Jank 的進一步加劇。
  2. 注意在第3段中,A幀的計算已完成,但是在第4個vsync來的時候才顯示,如果是雙緩沖,那在第三個vynsc就可以顯示了。

三緩沖有效利用了等待VSync的時間,減少了jank,但是帶來了延遲。是不是 Buffer 越多越好呢?這個是否定的, Buffer 正常還是兩個,當出現(xiàn) Jank 后三個足以。

Choreographer

上邊講的都是基礎的刷新知識,那么在 Android 系統(tǒng)中,真正來實現(xiàn)繪制的類叫Choreographer

Choreographer負責對CPU/GPU繪制的指導 —— 收到VSync信號才開始繪制,保證繪制擁有完整 16.6ms,避免繪制的隨機性。

通常 應用層不會直接使用Choreographer,而是使用更高級的API,例如動畫和View繪制相關的 ValueAnimator.start()、View.invalidate()等。

(這邊補充說一個面試題,屬性動畫更新時會回調onDraw嗎?不會,因為它內部是通過AnimationHandler中的Choreographer機制來實現(xiàn)的更新,具體的邏輯,如果以后有時間的話可以寫篇文章來說一說。)

業(yè)界一般通過Choreographer來監(jiān)控應用的幀率。

(這個東西也是個面試題,會問你如何檢測應用的幀率?你可以提一下Choreographer里面的FrameCallback,然后結合一些第三方庫的實現(xiàn)具體說一下。)

View刷新的入口

Activity啟動,走完onResume方法后,會進行window的添加。window添加過程會調用ViewRootImpl的setView()方法, setView()方法會調用requestLayout()方法來請求繪制布局,requestLayout()方法內部又會走到scheduleTraversals()方法。最后會走到performTraversals()方法,接著到了我們熟知的測量、布局、繪制三大流程了。

當我們使用 ValueAnimator.start()、View.invalidate()時,最后也是走到ViewRootImpl的 scheduleTraversals()方法。(View.invalidate()內部會循環(huán)獲取ViewParent直到ViewRootImpl的invalidateChildInParent()方法,然后走到scheduleTraversals(),可自行查看源碼)

即所有UI的變化都是走到ViewRootImpl的scheduleTraversals()方法。

這里注意一個點:scheduleTraversals()之后不是立即就執(zhí)行performTraversals()的,它們中間隔了一個Choreographer機制。簡單來說就是scheduleTraversals()中,Choreographer會去請求native的VSync信號,VSync信號來了之后才會去調用performTraversals()方法進行View繪制的三大流程。


 //ViewRootImpl.java
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //添加同步屏障,屏蔽同步消息,保證VSync到來立即執(zhí)行繪制
        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到來后立即執(zhí)行繪制
  2. mChoreographer.postCallback()方法,發(fā)送一個會在下一幀執(zhí)行的回調,即在下一個VSync到來時會執(zhí)行 TraversalRunnable–>doTraversal()—>performTraversals()–>繪制流程。

Choreographer

初始化

mChoreographer,是在ViewRootImpl的構造方法內使用 Choreographer.getInstance()創(chuàng)建。

Choreographer和Looper一樣是線程單例的,通過ThreadLocal機制來保證唯一性。因為Choreographer內部通過FrameHandler來發(fā)送消息,所以初始化的時候會先判斷當前線程有無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) {
         //當前線程要有l(wèi)ooper,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)方法,第一個參數(shù)是CALLBACK_TRAVERSAL,表示回調任務的類型,共有以下5種類型:

//輸入事件,首先執(zhí)行
public static final int CALLBACK_INPUT = 0; 
//動畫,第二執(zhí)行
public static final int CALLBACK_ANIMATION = 1; 
//插入更新的動畫,第三執(zhí)行
public static final int CALLBACK_INSETS_ANIMATION = 2; 
//繪制,第四執(zhí)行
public static final int CALLBACK_TRAVERSAL = 3; 
//提交,最后執(zhí)行,
public static final int CALLBACK_COMMIT = 4;
復制代碼

五種類型任務對應存入對應的CallbackQueue中,每當收到 VSYNC 信號時,Choreographer 將首先處理 INPUT 類型的任 務,然后是 ANIMATION 類型,最后才是 TRAVERSAL 類型。

postCallback()內部調用postCallbackDelayed(),接著又調用postCallbackDelayedInternal(),正常消息執(zhí)行scheduleFrameLocked,延遲運行的消息會發(fā)送一個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) { //立即執(zhí)行
             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:
                // 執(zhí)行doFrame,即繪制過程 
                doFrame(System.nanoTime(), 0);
                break;
            case MSG_DO_SCHEDULE_VSYNC: 
                //申請VSYNC信號,例如當前需要繪制任務時 
                doScheduleVsync();
                break;
            case MSG_DO_SCHEDULE_CALLBACK: 
                //需要延遲的任務,最終還是執(zhí)行上述兩個事件 
                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) {
            //當前執(zhí)行的線程,是否是mLooper所在線程
            if (isRunningOnLooperThreadLocked()) {
                //申請 VSYNC 信號
                scheduleVsyncLocked();
            } else {
                // 若不在,就用mHandler發(fā)送消息到原線程,最后還是調用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信號的注冊和監(jiān)聽是通過mDisplayEventReceiver實現(xiàn)的。mDisplayEventReceiver是在Choreographer的構造方法中創(chuàng)建的,是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信號監(jiān)聽者
    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, 發(fā)消息后 會走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()回調也會執(zhí)行嗎?不會,因為VSync是UI需要刷新的時候主動去申請的,而不是native層不停地往上面去推這個回調的,這邊要注意。)

doFrame

doFrame()方法中會通過doCallbacks()方法去執(zhí)行各種callbacks,主要內容就是取對應任務類型的隊列,遍歷隊列執(zhí)行所有任務,其中就包括了 ViewRootImpl 發(fā)起的繪制任務mTraversalRunnable了。mTraversalRunnable執(zhí)行doTraversal()方法,移除同步屏障,調用performTraversals()開始三大繪制流程。

到這里整個流程就閉環(huán)了。

本文在開源項目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄,里面包含了Android組件化最全開源項目(美團App、得到App、支付寶App、微信App、蘑菇街App、有贊APP...)等,資源持續(xù)更新中...

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容