程式設計師必懂的Android 技術之 VSYNC、 Choreographer 起源

椰果玩安卓 發佈 2020-08-14T14:38:33+00:00

其中,Google 在 2012 年的 I/O 大會上宣布了 Project Butter 黃油計劃,那個曾經嚴重影響 Android 口碑的 UI 流程性問題,首先在這得到有效的控制,並且在 Android 4.1 中正式開啟了這個機制。

1

Project Butter


現在我們已經很少能夠聽到關於 Android UI 卡頓的話題了,這得益於 Google 長期以來對 Android 渲染性能的重視,基本每次 Google I/O 都會花很多篇幅講這一塊。隨著時間的推移,Android 系統一直在不斷進化、壯大,並且日趨完善。


其中,Google 在 2012 年的 I/O 大會上宣布了 Project Butter 黃油計劃,那個曾經嚴重影響 Android 口碑的 UI 流程性問題,首先在這得到有效的控制,並且在 Android 4.1 中正式開啟了這個機制。


Project Butter 對 Android Display 系統進行了重構,引入了三個核心元素,即 VSYNC、Triple Buffer 和 Choreographer。


其中 VSYNC 是理解 Project Butter 的核心。接下來,我們就圍繞 VSYNC 開始介紹 Project Butter 對 Android Display 系統做了哪些優化。


2

VSYNC


VSYNC 最初是由 GPU 廠商開發的一種,用於防止螢幕撕裂的技術方案,全稱 Vertical Synchronization,該方案很早就已經被廣泛應用於 PC 上。我們可以把它理解為一種時鐘中斷。


1. 前世

VSYNC 是一種圖形技術,它可以同步 GPU 的幀速率和顯示器的刷新頻率,所以在理解 VSYNC 產生的原因及其作用之前,我們有必要先來了解下這兩個概念。


刷新頻率(Refresh Rate)


表示螢幕在一秒內刷新畫面的次數, 刷新頻率取決於硬體的固定參數,單位 Hz(赫茲)。例

如常見的 60 Hz、144 Hz,即每秒鐘刷新 60 次或 144 次。


逐行掃描


顯示器並不是一次性將畫面顯示到螢幕上,而是從左到右邊,從上到下逐行掃描顯示,不過這一過程快到人眼無法察覺到變化。以 60 Hz 刷新率的螢幕為例,即 1000 / 60 ≈ 16ms。


幀速率 (Frame Rate)


表示 GPU 在一秒內繪製操作的幀數,單位 fps。例如在電影界採用 24 幀的速度足夠使畫面運行的非常流暢。而 Android 系統則採用更加流暢的 60 fps,即每秒鐘繪製 60 幀畫面。

螢幕撕裂

現在,刷新頻率和幀率需要一起合作,才能使圖形內容呈現在螢幕上,GPU 會獲取圖形數據進行繪製, 然後硬體負責把圖像內容呈現到螢幕上,這一過程在應用程式的生命周期內一遍又一遍的發生。


如上圖,CPU / GPU 生成圖像的 Buffer 數據,螢幕從 Buffer 中讀取數據刷新後顯示。


理想情況下幀率和刷新頻率保持一致,即每繪製完成一幀,顯示器顯示一幀。不幸的是,刷新頻率和幀率並不總是能夠保持相對同步,如果幀速率實際比刷新率快,例如幀速率是 120 fps,顯示器的刷新頻率為 60 Hz。此時將會發生一些視覺上的問題


當 GPU 利用一塊內存區域寫入一幀數據時,從頂部開始新一幀覆蓋前一幀,並立刻輸出一行內容。當螢幕刷新時,此時它並不知道圖像緩衝區的狀態,因此從緩衝區抓取的幀並不是完整的一幀畫面(繪製和螢幕讀取使用同一個緩衝區)。


此時螢幕顯示的圖像會出現上半部分和下半部分明顯偏差的現象,這種情況被稱之為 「tearing」(螢幕撕裂)。


發生 「tearing」 現象的根源是由於幀速率與刷新頻率不一致導致。


Double Buffer(雙重緩存)

那如何防止 「tearing」 現象的發生呢?由於圖像繪製和讀取使用的是同一個緩衝區,所以螢幕刷新時可能讀取到的是不完整的一幀畫面。


解決方案是採用 Double Buffer。


Double Buffer(雙緩衝)背後的思想是讓繪製和顯示器擁有各自的圖像緩衝區。


GPU 始終將完成的一幀圖像數據寫入到 Back Buffer,而顯示器使用 Frame Buffer,當螢幕刷新時,Frame Buffer 並不會發生變化,Back Buffer 根據螢幕的刷新將圖形數據 copy 到 Frame Buffer,這便是 VSYNC 的用武之地。


注意,VSYNC 信號負責調度從 Back Buffer 到 Frame Buffer 的交換操作,這裡並不是真正的數據 copy,實際是交換各自的內存地址,可以認為該操作是瞬間完成。


在 Android 4.1 之前,Android 便使用的雙緩衝機制。怎麼理解呢?


一般來說,在同一個 View Hierarchy 內的不同 View 共用一個 Window,也就是共用同一個 Surface。


每個 Surface 都會有一個 BufferQueue 緩存隊列,但是這個隊列會由 SurfaceFlinger 管理,通過匿名共享內存機制與 App 應用層交互


整個流程如下:


  • 每個 Surface 對應的 BufferQueue 內部都有兩個 Graphic Buffer,一個用於繪製一個用於顯示。系統會把內容先繪製到離屏緩衝區(OffScreen Buffer),在需要顯示時,才把離屏緩衝區的內容通過 Swap Buffer 複製到 Front Graphic Buffer 中。
  • 這樣 SurfaceFlinge 就拿到了某個 Surface 最終要顯示的內容,但是同一時間我們可能會有多個 Surface。這裡面可能是不同應用的 Surface,也可能是同一個應用裡面類似 SurfaceView 和 TextureView,它們都會有自己獨立的 Surface。
  • 這個時候 SurfaceFlinger 把所有 Surface 要顯示的內容統一交給 Hardware Composer,它會根據位置、Z-Order 順序等信息合成為最終螢幕需要顯示的內容,而這個內容會交給系統的幀緩衝區 Frame Buffer 來顯示(Frame Buffer 是非常底層的,可以理解為螢幕顯示的抽象)。

Android 一直使用 VSYNC 來防止螢幕畫面發生撕裂現象

2. 今生

但是 UI 繪製任務可能會因為 CPU 在忙別的事情,導致沒來得及處理。所以從 Android 4.1 開始, VSYNC 則更進一步,現在 VSYNC 脈衝信號開始用於下一幀的所有處理。


Project Butter 首先對 Android Display 系統的 SurfaceFlinger 進行了改造,目標是提供 VSYNC 中斷。沒收到 VSYNC 中斷後,CPU 會立即準備 Buffer 數據,由於大部分顯示設備刷新頻率都是 60 Hz(一秒刷新 60 次),也就是說一幀數據的準備工作都要在 16ms 內完成


這樣應用總是在 VSYNC 邊界上開始繪製,而 SurfaceFlinger 總是在 VSYNC 邊界上進行合成。這樣可以消除卡頓,並提升圖形的視覺表現。


Triple Buffer(三重緩存)

如果理解了雙緩衝機制的原理,那就非常容易理解什麼是三緩衝區了。如果只有兩個 Graphic Buffer 緩衝區 A 和 B,如果 CPU / GPU 繪製過程較長,超過一個 VSYNC 信號周期。


由上圖可知:

  • 在第二個 16 ms 時間段內,Display 本應該顯示 B 幀,但卻因為 GPU 還在處理 B 幀,導致 A 幀被重複顯示。
  • 同理,在第二個 16 ms 時間段內,CPU 無所事事,因為 A Buffer 被 Display 在使用。B Buffer 被 GPU 在使用。注意,一旦過了 VSYNC 時間點,CPU 就不能被觸發處理繪製工作了。


為什麼 CPU 不能在第二個 16ms 處理繪製工作呢?


原因是只有兩個 Buffer,緩衝區 B 中的數據還沒有準備完成,所以只能繼續展示 A 緩衝區的內容,這樣緩衝區 A 和 B 都分別被顯示設備和 GPU 占用,CPU 則無法準備下一幀的數據。

如果再提供一個緩衝區,CPU、GPU 和顯示設備都能使用各自的緩衝區工作,互不影響。

簡單來說,三重緩衝機制就是在雙緩衝機制基礎上增加了一個 Graphic Buffer 緩衝區,這樣可以最大限度的利用空閒時間,帶來的壞處是多使用的一個 Graphic Buffer 所占用的內存


由上圖可知:


  • 在第二個 16ms 時間段,CPU 使用 C Buffer 完成繪圖工作,雖然還是會多顯示一次 A 幀,但後續顯示就比較順暢了,有效避免 Jank 的進一步加劇。
  • 注意:是不是 Buffer 越多越好呢?這個是否定的,Buffer 正常還是兩個,當出現 Jank 後三個足以。


3、Choreographer


Choreographer 也是 Project Butter 計劃新增的機制,用於配合系統的 VSYNC 中斷信號。


它本質是一個 Java 類,如果直譯的話為舞蹈指導,這是一個極富詩意的表達,看到這個詞不得不讚嘆設計者除了 Coding 之外的廣泛視野。


舞蹈是有節奏的,節奏使舞蹈的每個動作更加協調和連貫;視圖刷新也是如此。


Choreographer 可以接收系統的 VSYNC 信號,統一管理應用的輸入、動畫和繪製等任務的執行時機。Android 的 UI 繪製任務將在它的統一指揮下,井然有序的完成。業界一般通過它來監控應用的幀率。


Choreographer 的構造方法:

private Choreographer(Looper looper, int vsyncSource) {
        // 當前線程的Looper
        mLooper = looper;
        // 創建該Looper的Handler
        mHandler = new FrameHandler(looper);
        // 是否開啟VSYNC,開啟VSYNC後將通過FrameDisplayEventReceiver接受
        // VSYNC脈衝
        mDisplayEventReceiver = USE_VSYNC
                ? new FrameDisplayEventReceiver(looper, vsyncSource)
                : null;
        mLastFrameTimeNanos = Long.MIN_VALUE;

        // 計算一幀的時間
        // Android手機螢幕採用60Hz的刷新頻率
        // 這裡是納秒 ≈16000000ns 還是16ms
        mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
        // 創建一個CallbackQueu的數組,默認為4
        // CallbackQueue中存放要執行的輸入、動畫、遍歷繪製等任務
        // 也就是 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL
        mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
        for (int i = 0; i <= CALLBACK_LAST; i++) {
            mCallbackQueues[i] = new CallbackQueue();
        }
        // b/68769804: For low FPS experiments.
        setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}

Choreographer 是線程單例的,而且必須要和一個 Looper 綁定,因為其內部有一個 Handler 需要和當前繪製線程的 Looper 綁定。


DisplayEventReceiver 是一個 abstract class,在其構造方法內會通過 JNI 創建一個 IDisplayEventConnection 的 VSYNC 的監聽者。


另外 DisplayEventReceiver 中包含兩個非常重要的方法:一個用於需要繪製任務時,申請 VSYNC 信號的 scheduleVsync 方法,另一個用於接收 VSYNC 信號的 onVsync 方法。FrameDisplayEventReceiver 是 DisplayEventReceiver 的唯一實現類,並重寫 onVsync 方法用於通知 Choreographer。


Choreographer 的主要功能是,當收到 VSYNC 信號時,去調用通過 postCallback 設置的回調方法。目前一共定義了四種類型的回調,它們分別是:

  1. CALLBACK_INPUT:優先級最高,和輸入事件處理有關;
  2. CALLBACK_ANIMATION:優先級其次,和 Animation 的處理有關;
  3. CALLBACK_TRAVERSAL:優先級最低,和 UI 等空間繪製有關;
  4. CALLBACK_COMMIT:最後執行,和提交任務有關(在 API Level 23 添加)。


優先級的高低和處理順序有關。


當收到 VSYNC 信號時,Choreographer 將首先處理 INPUT 類型的回調,然後 ANIMATION 類型,最後才是 TRAVERSAL 類型。


另外,Android 在 4.1 還對 Handler 機制進行了略微改造,使之支持 Asynchronous Message(異步消息) 和 Synchronization Barrier(同步屏障)。


一般情況下同步消息和異步消息的處理方式並沒有什麼區別,


只有在設置了同步屏障時才會出現差異。


同步屏障為 Handler 消息機制增加了一種簡單的優先級關係,異步消息的優先級要高於同步消息。


簡單點說,設置了同步屏障之後,Handler 只會處理異步消息。


以 View 的繪製流程為例:

void scheduleTraversals() {    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 同步屏障,阻塞所有的同步消息
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 將 UI 繪製任務發送到 Choreograhper
        // 注意mTraversaRunnable是一個Runnable對象
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        // ...
    }
}

scheduleTraversals 首先禁止了後續消息的處理能力,一旦設置了消息隊列的 postSyncBarrier,所有非 Asynchronous 的消息將被停止派發。


UI 繪製任務設置了 CALLBACK 類型為 TRAVERSAL 類型的任務,即 mTraversalRunnable:

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        //開始執行繪製遍歷
        doTraversal();
    }
}

Choreographer 的 postCallback 方法將會申請一次 VSYNC 中斷信號,通過 DisplayEventReceiver 的 scheduleVsync 方法。當 VSYNC 信號到達時,便會回調 Choreographer 的 doFrame 方法,內部會觸發已經添加的回調任務:

// 回調 INPUT 任務
doCallbacks(Choreographer.CALLBACK_INPUT, mframeTimeNanos);
// 回調 ANIMATION
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 回調 View 繪製任務 TRAVERSAL
doCallbacks(Choreographer,CALLBACK_TRAVERSAL, frameTimeNanos);
// API Level 23 新增,COMMIT 
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);

此時 UI 繪製任務 doTraversal 方法被回調,即在 Android 4.1 之後, UI 繪製任務被放置到了 VSYNC 中斷處理中了。Choreographer 確實做到了統一協調管理 UI 的繪製工作。

有關於參考Android核心知識點筆記github:https://github.com/AndroidCot/Android

關鍵字: