Overview
多年前Android的UI流暢性差的問題一直飽受詬病,Google為了解決這個問題開發了Project Butter項目,也就是黃油計划,期望徹底改善Android系統的流暢性。這是Android UI系統的一次非常大的改進,學習如何改進,是我們掌握Android渲染機制的關鍵。
概括來說在這次改進中,Google打出了一套VSync+Choreographer+TripleBuffer的組合拳。
●VSync:它是黃油計划的核心,VSync(Vertical Synchronization 垂直同步)是一種在PC時代就廣泛使用的技術,簡單說來它是利用屏幕刷新的間隙來進行幀緩沖區交換的技術。
●Choreographer:Choreographer引入是為了配合VSync,給App端一個穩定渲染處理時機。
●TripleBuffer
VSync
大家知道屏幕上的畫面每一幀都是靜態的,不同的幀不斷的進行刷新我們才能看的動態的畫面,對於Android來說,界面的刷新是16.67ms刷新一次,也就是60fps(frame per second,每秒更新60次),為什么是60fps呢?
60fps是跟硬件屏幕的刷新率有關的,為了和主流屏幕的刷新率保持一致,目前主流的屏幕是60HZ(當然現在還有90HZ,120HZ),對於屏幕來說更高的刷新率就意味着更高的功耗,以及更短的TFT數據寫入時間,對屏幕來說,設計難度也就更大。
那么Android系統,怎么和屏幕刷新做同步呢,這就要提到VSync機制,什么是VSync機制?
簡單來說,VSync機制就是把UI體系的FPS和顯示器的刷新頻率同步起來,避免屏幕界面出現“斷裂”的現象。
我們以VSync為中心,來用一組圖來解釋這幾個概念,這些圖來自於Google I/O 2012關於Project Butter的主題演講。
●黃色:顯示器
●綠色:GPU
●藍色:CPU
三者被等分成16.67ms的段,來觀察每個周期內的渲染情況。

當不使用VSync機制時,在標號2中由於CPU沒有及時處理這一幀,導致顯示器只能繼續顯示第一幀,這種情況稱為Jank。為什么CPU沒有及時處理呢,可能是CPU在忙於處理其他事情,忘記了處理UI繪制。當CPU想起來要去處理UI繪制時,又過了最佳時間段,
為此就引入了Vsync,它類似於一種中斷機制,通知CPU去處理UI繪制。如下所示:

由於使用了VSync機制,CPU可以早早的開始處理標號2的繪制,這樣顯示器也能正常的顯示標號2的數據了。
這似乎非常完美,由於由VSync這種同步機制的存在CPU/GPU與顯示器的FPS都保持了一致,如我們上文所說是60fps。負責在VSync信號到來時,實現這種同步工作的正是Choreographer。
但是這都建立在CPU和GPU都能在一個周期內(16.67ms)完成自己的工作,如果不能呢,如下所示:

GPU沒有在一個周期內處理完B,導致Display只能繼續顯示A,這樣AB兩個Buffer(Android早期是Double Buffer機制,一個Back Buffer供GPU和CPU使用,一個Front Buffer供Display使用)都被占用了。這樣就導致CPU在第二個周期內無所事事。不僅如此,由於CPU在等待A釋放,導致CPU繪制被延期,有導致了下一個Jank。
這個時候如果有第三個Buffer,CPU就能去干活了。這也就是Tripple Buffer機制。如下所示:

CPU使用第三個Buffer C進行繪制,雖然A在Display里還是重復顯示了一次,但是后續的繪制流程就比較流暢了。那Buffer是不是越多越好呢,答案是並不是,CPU繪制的Buffer C數據要等待第四個周期才顯示,這比Double Buffer多了16.67ms的延遲,正常情況下都是兩個或者三個(Chromium就是Double Buffer機制)。
什么是VSync機制?
VSync(Vertical Synchronization 垂直同步)可以同步應用渲染、屏幕合成以及屏幕刷新周期的時間,消除卡頓,提升圖形的視覺表現。VSync信號由HardwareCompositor產生,它封裝了硬件廠商提供的HAL層,如果HAL層能產生VSync信號,則直接使用硬件VSync信號,否則使用HardwareCompositor內部的VSyncThread模擬產生軟件VSync信號(Sleep固定時間,然后喚醒)。
HardwareCompositor產生HW_VSYNC信號,經由DispSync生成SW_VSYNC信號,SW_VSYNC信號經過offset調整生成了兩種信號:

●VSYNC_APP:Choreographer使用,Choreographer會配合VSync,給上層App一個穩定的渲染知己,上面提到VSync的觸發周期是16.67ms,每隔16.67ms,VSync信號就會喚醒Choreographer來做App的繪制操作。
●VSYNC_SF:SurfaceFlinger使用。SurfaceFlinger會在VSync信號到來的時候,進行合成操作。
其中phase-sf和phase-app就是一個vsync offset(0-16.67ms),這個值可以通過adb shell dumpsys surfaceflnger獲得。

什么是vsync offset,簡單來說它是VSync信號的偏移,正常情況下VSYNC-app和VSYNC-sf信號時同步到來的,一個生產當前幀的數據,一個消費上一幀的數據,VSync Offset可以讓VSYNC信號發生偏移,比如讓VSYNC-sf信號提前到來,提前處理當前這一幀的數據。
有了VSync機制,Android UI系統就可以在VSync信號的驅動下有條不紊的進行渲染和刷新了。我們來看看具體的實現。
Choreographer
相關源碼
●Choreographer.java
Chroreographer的引入主要是為了配合VSync信號,給上層App渲染一個穩定的Message處理時機,它在Android渲染流水線中扮演着承上啟下的角色。
●向下負責接收和處理App的各種頁面更新消息和回調(例如Input、Animation、Traversal等),等到VSync到來的時候統一處理,以及判斷卡頓掉幀情況,記錄回調耗時等。
●向下負責請求(FrameDisplayEventReceiver.scheduleVsync,當應用需要繪制UI時,會申請一次VSync中斷,然后再在中斷處理的onVSync函數中進行繪制)和接收(FrameDisplayEventReceiver.onVsync())VSync信號。
工作流程
1Choreographer初始化,初始化FrameHandler,綁定Looper;初始化FrameDisplayEventReceiver,它會創建一個IDisplayEventConnection的VSync監聽者對象,與SurfaceFlinger監理通信用來請求和接收VSync信號。
2SurfaceFlinger的appEventThread喚醒並發送VSync信號,觸發Choreographer回調FrameDisplayEventReceiver.onVsync(),進入Choreographer的主要處理函數doFrame()。
3Choreographer計算掉幀邏輯。
4Choreographer處理Input回調。
5Choreographer處理Animation回調。
6Choreographer處理Insets Animation回調。
7Choreographer處理Traversal回調。
8Choreographer處理Commit回調。
9RenderThread處理繪制數據,執行渲染。
10RenderThread將處理好的Buffer提交給SurfaceFlinger進行合成。
這一塊的流程我們會在下面展開講。
TripleBuffer
Triple Buffer簡單說來是使用三個Buffer進行輪轉,如下所示:

我們一直提到Buffer,那什么是Buffer?
Buffer/FrameBuffer是內核系統提供給圖形硬件的抽象描述,之所以稱為buffer,是因為他占用了系統存儲空間的一部分,是一塊包含屏幕顯示信息的緩沖區。簡單來說FrameBuffer代表了屏幕即將要顯示的一幀畫面。
在Android系統中,FrameBuffer提供的設備節點是/dev/graphics/fb*,fb按照順序排列,支持多個屏幕,例如fb0表示主屏幕。而負責管理FrameBuffer緩沖區的正是Gralloc,它提供了對FrameBuffer緩沖區統一的管理和訪問。
Gralloc通過一種BufferQueue的機制來管理FrameBuffer緩沖區。
什么是BufferQueue?
BufferQueue是渲染數據流經的通道,一般由消費者創建,而生產者一般不和BufferQueue在同一個進程里,如圖所示這里的消費者就是SurfaceFlinger,生生產者有多個來源。

圖形
BufferQueue工作流如下所示:

圖形
1dequeue:生產者發起,當生產者需要緩沖區時,它會通過調用dequeueBuffer()向BufferQueue請求一個可用的緩沖區,並指定緩沖區的寬度、高度、像素格式和使用標記。
2queue:生產者發起,當生產者需要填充緩沖區時,它會通過調用queueBuffer()將緩沖區返回到BufferQueue。
3acquire:消費者發起,消費者通過acquireQueue()從BufferQueue獲取緩沖區並使用緩沖區內的內容。
4release:消費者發起,消費者操作完成以后,通過調用releaseBuffer()將該緩沖區返回到BufferQueue。
Rendering Architecture

UI Framework
上圖的右邊顯示了一個基本的Android頁面包含哪些元素,直觀上看一個Android界面至少由Activity、Window、View三部分構成,它們相互陪配合渲染出可供用戶交互的圖形界面。
Activity、Window、View三者關聯的類圖如下所示:

這張類圖大體可以分為Activity、Window、View、WMS四大塊構成。以下東西涉及知識點繁雜,如果感興趣可以深入學習,也可直接略過
●Activity:圖形系統的頂級容器,繼承自ContextThemeWrapper,直接面向用戶,負責頁面的生命周期管理,接收用戶的輸入。
○【變量關聯-上下文】Activity組件在啟動的時候,系統會為它創建一個ContextImpl對象,通過Activity.attach()方法關聯到Activity,並通過ContextThemeWrapper.attachBaseContext()和ContextWrapper.attachBaseContext()方法關聯到它們的mBase變量;系統還會調用ContextImpl.setOuterContext()方法來將Activity組件關聯到其成員變量mOuterContext上。
○【變量關聯-窗口】Activity的成員變量mWindow指向Window,而Window的成員變量mContext和mCallback都指向了Activity。
●Window:圖形系統的窗口,它的主要功能是描述窗口信息,用來管理View。Window的實際實現類是PhoneWindow,由PolicyManager.makeNewWindow()創建。
○【變量關聯-視圖管理】Window的成員變量mWindowManager指向WindowManagerImpl對象,該對象內部有個WindowManagerGlobal,它也是實際的實現類,該類內部維護了VIew、VIewRootImpl和LayoutParams三個數組,用來管理View。
○【變量關聯-視圖容器】PhoneWIndow內部有兩個關鍵的變量DecorView mDecor和ViewGroup mContentParent,mDecor是應用的頂級視圖,mContentParent是視圖的父容器,一般是mDrcor自身或者是它的子視圖。
●View:圖形界面的視圖,它內部的draw()方法用來繪制視圖,而控制繪制的是ViewRootImpl對象,它繼承於Handler,
○【變量關聯-繪制控制器】ViewRootImpl內部的mView變量指向的就是DecorView,它會調用DecorView.draw()方法來控制繪制。
●ViewRootImpl:圖形系統的繪制控制器,每一個Activity組件都由一個對應的ViewRootImpl對象、頂級VIew對象、和WIndowManager.LayoutParams對象。這也是WindowManagerGlobal里三個數組的來源。ViewRootImpl一方面利用內部對象sWindowSession與WMS進行雙向通信,另一方面,ViewRootImpl作為Handler的實現類,還負責向主線程發送消息。
○【消息分發-輸入事件】當InputManager接收到鍵盤、觸摸屏等輸入事件事,ViewRootImpl會把這些事件封裝成一個消息,發送到主線程中進行處理。
○【消息分發-繪制事件】當需要重新繪制它關聯的一個View時,ViewRootImpl會把繪制操作封裝成一個消息發送到主線程中進行處理。
●Surface:圖形系統的連接器,每個ViewRootImpl對象內部都有個Surface對象,這個Java層的Surface對象,其成員變量mNativeSurface指向了一個C++層的Surface對象。C++層的Surface對象負責向應用窗口的圖形緩沖區填充UI數據,即設置窗口的紋理,這些紋理保存中Surface類的成員變量mCanvas中,即通過這個畫布就可以訪問圖形緩沖區。
●Canvas:圖形系統的畫布,ViewRootImpl在執行繪制指令時,會先獲取Surface的Canvas畫布對象,然后傳遞給VIew.draw()方法,然后就可以在Canvas上進行繪制了。實際的繪制工作是由底層的Skia圖形庫完成的。
●WindowManager:Window需要與WMS通信,但是它並沒有直接實現這個功能,因為一個應用中可能存在多個Window,每個Window需要與WMS單獨通信,會造成資源浪費和管理混亂,因此這是便有了WindowManager,它也是Window內部的成員變量mWindowManager。
●WindowManagerImpl:WindowManager的子類,它內部有一個WindowManagerGlobal對象,負責管理View、ViewRootImpl和LayoutParams的數組對象。
●WindowManagerService:窗口管理服務,負責創建和管理WIndow。
以上便是Android的UI Framework大致的構成,有助於我們了解后面的流程。
Structure
從結構上看,Android的渲染時UI Thread和Render Thread相互配合完成的。
●UI Thread:UI Thread負責生成繪制指令,同步給Render Thread。
●Render Thread:Render Thread對這些繪制指令,調用OpenGL方法,將生產的Frame Buffer提供給SurfaceFlinger進行合成,最終輸出到屏幕上顯示。
Flow
從流程上看,Android UI系統在VSync信號的驅動下有條不紊的進行繪制和合成的工作。VSYNC_APP信號控制着App端的繪制,源源不斷的生產buffer數據,VSYNC_SF信號則控制着SurfaceFlinger消費buffer數據,合成上屏。
流程圖如下所示:

1App端第一N次接收SYNC-app信號后,回調Choreographer.onVsync()方法開始App的第N幀的渲染。開始依次處理Input、Animate、Traversal(Measure、Layout、Draw)等回調。生產繪制指令。
2App端完成這一幀的繪制后,會將繪制指令保存在Buffer中,放入BufferQueue。這樣當前緩存的幀數據就是加一。
3SurfaceFlinger端第N次接收到SYNC-sf信號后,開始處理這一幀的合成工作,消費掉這一幀的數據,當前緩存的幀數據就會減一。
以上便是一幀數據的生產與消費的過程,App端和SurfaceFlinger端一前一后,井然有序的生產和消費者幀數據,進行界面的渲染。
注:理想情況下,這種一前一后的方式看起來沒問題。真實的情況是SurfaceFlinger需要buffer數據的時候,App端可能還沒渲染好,這樣就可以出現掉幀(jank),所以Android引入了Tripple Buffer等緩存機制,這個我們后面會講。
以上面三個流程可以做進一步抽象:
1UI Thread進行繪制,並將數據同步給Render Thread。
2Render Thread將buffer數據提交給SurfaceFlinger。
3SurfaceFlinger/HW進行合成上屏。
前兩步可以歸為Rendering Pipeline,后兩步可以歸為Graphics Pipeline。我們接着來看。
Rendering Pipeline
Rendering Pipeline由UI Thread和Render Thread配合完成,如下所示:

App端在VSYNC-app信號的驅動下開始進行繪制工作,整個流程由兩大線程配合工作,核心入口如下:
●UI Thread:Choreographer.doFrame()
●Render Thread:DrawFrame
具體流程如下:
1input,處理Input回調。
2animation,處理Animation回調。
3traversal,處理Traversal回調,它會相繼調用Measure(測量)、Layout(布局)、Draw(構建DisplayList,里面包含OpenGL渲染所需的命令與數據)。
4syncFrameState,主線程與渲染線程同步(sync)繪制信息。
5dequeue buffer:從SurfaceFlinger的BufferQueue取出一個buffer(兩者不在同一進程,因而它是一個Binder調用),調用OpenGL相關函數,執行真正的渲染操作,最后這個渲染好的buffer會通過queue buffer放回BufferQueue。
6flush draw commands,渲染線程執行繪制操作,生成OpenGL指令。
7eglSwapBufferWIthDamageKHR:調用queue buffer將剛剛取出的buffer放回BufferQueue,供SurfaceFlinger消費。
在這個流程的前半段,Choreographer.doFrame()的前半段,它優先處理了三個回調:
●Input:用戶輸入
●Animation:動畫
●Traversal:繪制
這三個回調是由優先級的,優先級就是按照他們的執行順序排布的,也就是說Android系統中,響應用戶輸入是第一位的,接着是動畫,最后才是繪制。這個在Flutter等系統中也是一樣的。
Traversal回調由Choreographer.doFrame()發起,最終在ViewRootImpl.performTraversal()中被執行,它包含了Android UI系統的三個核心操作:

●Measure:測量View及其子View的大小。
●Layout:測量View及其子View的位置。
●Draw:繪制View及其子View
了解了Rendering Pipeline中關鍵的步驟,我們分別來看看它們的實現。
1 Input
處理用戶輸入回調。
2 Animation
處理動畫回調。
3 Measure
在分析Measure流程之前,我們先來了解一下Measure、Layout、Draw的整體流程。

我們接着來說Measure。
Measure是用來測量View及其子View的大小。View是構成一個Android界面的基本元素。
View是一個矩形區域,它有自己的位置、大小與邊距,如下所示:

Link
●Position:有左上角坐標(getLeft(), getTop())決定,該坐標是以它的父View的左上角為坐標原點,單位是pixels。
●Size:View的大小有兩對值來表示。getMeasuredWidth()/getMeasuredHeight()這組值表示了該View在它的父View里期望的大小值,在measure()方法完成后可獲得。getWidth()/getHeight()這組值表示了該View在屏幕上的實際大小,在draw()方法完成后可獲得。
●Padding:View的內邊距用padding來表示,它表示View的內容距離View邊緣的距離。通過getPaddingXXX()方法獲取。需要注意的是我們在自定義View的時候需要單獨處理padding,否則它不會生效,這一塊的內容我們會在View自定義實踐系列的文章中展開。
●Margin:View的外邊距用margin來表示,它表示View的邊緣離它相鄰的View的距離。Measure過程決定了View的寬高,該過程完成后,通常都可以通過getMeasuredWith()/getMeasuredHeight()獲得寬高。
測量的過程便是為了計算這些數據,在做測量的時候,measure()方法被父View調用,在measure()中做一些准備和優化工作后,調用onMeasure()來進行實際的自我測量。對於onMeasure(),View和ViewGroup有所區別:
●View:View 在 onMeasure() 中會計算出自己的尺寸然后保存;
●ViewGroup:ViewGroup在onMeasure()中會調用所有子View的measure()讓它們進行自我測量,並根據子View計算出的期望尺寸來計算出它們的實際尺寸和位置然后保存。同時,它也會根據子View的尺寸和位置來計算出自己的尺寸然后保存。
MeasureSpec,它是一個32位int值。
●高2位:SpecMode,測量模式
○UNSPECIFIED:父View不對子View做任何限制,需要多大給多大,這種情況一般用於系統內部,表示一種測量的狀態。
○EXACTLY:父View已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值,它對應match_parent和指定大小這兩種情況。
○AT_MOST:父View給子VIew提供一個最大可用的大小,子View去自適應這個大小。它對於wrap_content這種情況。
●低30位:SpecSize,在特定測量模式下的大小。
4 Layout
在進行布局的時候,layout()方法被父View調用,在layout()中它會保存父View傳進來的自己的位置和尺寸,並且調用onLayout()來進行實際的內部布局。對於onLayout(),View和ViewGroup有所區別:
●View:由於沒有子 View,所以 View 的 onLayout() 什么也不做。
●ViewGroup:ViewGroup在onLayout()中會調用自己的所有子View的layout()方法,把它們的尺寸和位置傳給它們,讓它們完成自我的內部布局。
Layout流程就會涉及到一些布局算法了,Android上常用的布局類型如下所示:
●LinearLayout
●AdapterView
●RelativeLayout
●ConstraintLayout
●MotionLayout
它們都繼承於ViewGroup。
5 Draw
Android硬件加速是默認開啟的,因而Draw方法並沒有執行真正的繪制調用,而是把要繪制的內容記錄到DisplayList里面,然后同步到RenderThread中,一般同步完成,UI Thread就可以被釋放出來做其他事情,而Render Thread繼續執行渲染工作。
View的繪制流程如下所示:
1Draw the background
2If necessary, save the canvas' layers to prepare for fading
3Draw view's content
4Draw children
5If necessary, draw the fading edges and restore layers
6Draw decorations (scrollbars for instance)
引自View.java
這些繪制最終都會調用到View.invalidate()和View.invalidate()兩個方法。
6 Sync
接來下這塊會在Render Thread中執行,相關代碼如下:
●frameworks/base/libs/hwui/renderthread/

這部分代碼會運行在Render Thread下,它會執行一個DrawFrameTask,這個Task的核心方法就是DrawFrame,DrawFrame會執行一系列操作,如下所示:
這里首先會調用syncFrameState,在主線程與渲染線程之間同步(sync)繪制信息。
然后調用flush draw commands,渲染線程將數據上傳(ARM設備內存一般是CPU和GPU共享內存)給GPU。並通知SurfaceFlinger進行圖層合成。
從源碼的角度看圖形數據流的流向。

Graphics Pipeline
Android也是通過同步光柵化的方式進行柵格化和合成上屏。什么是同步光柵化?
同步光柵化
光柵化和合成在一個線程,或者通過線程同步等方式來保證光柵化和合成的的順序。
:
●直接光柵化:直接執行可見圖層的DisplayList中可見區域的繪制指令進行光柵化,在目標Surface的像素緩沖區上生成像素的顏色值。
●間接光柵化:為指定圖層分配額外的像素緩沖區(例如Android提供View.setLayerType允許應用為指定View提供像素緩沖區,Flutter提供了Relayout Boundary機制來為特定圖層分配額外緩沖區),該圖層光柵化的過程中會先寫入自身的像素緩沖區,渲染引擎再將這些圖層的像素緩沖區通過合成輸出到目標Surface的像素緩沖區。
異步分塊光柵化
圖層會按照一定的規則粉塵同樣大小的圖塊,光柵化以圖塊為單位進行,每個光柵化任務執行圖塊區域內的指令,將執行結果寫入分塊的像素緩沖區,光柵化和合成不在一個線程內執行,並且不是同步的。如果合成過程中,某個分塊沒有完成光柵化,那么它會保留空白或者繪制一個棋盤格圖形。
Android和Flutter采用同步光柵化策略,以直接光柵化為主,光柵化和合成同步執行,在合成的過程中完成光柵化。而Chromium采用異步分塊光柵化測量,圖層會進行分塊,光柵化和合成異步執行。
9 Raster&Compositing
App端生產的FrameBuffer數據會在SurfaceFlinger/HW端進行消費,兩個通過BufferQueue來傳遞數據。
上面提到了兩個角色:
●SurfaceFlinger:負責接收多個來源的數據緩沖區,對它們進行合成,然后發送到顯示設備。
●HWCompositor:
具體流程如下:

SurfaceFinger主線程接收到VSYNC-sf信號以后,開始合成圖層,如果之前的GPU渲染任務還沒有結束,則等待GPU渲染完成再進行合成(Fence機制)。處理合成的主要是主線程里的onMessageReceived(),它會處理以下消息:
1執行handleMessageTransaction()和handleMessageInvalidate()方法,處理MessageQueue::INVALIDATA消息。
2執行handleMessageRefresh()方法,處理MessageQueue::Refresh消息。
○准備工作:preComposition() -> rebuildLayerStacks() -> calculateWorkingSet()
○合成工作:beiginFrame() -> prepareFrame() -> doDebugFlashRegions() -> doComposition()
○收尾工作:logLayerStats() -> postFrame() -> postComposition()
SurfaceFlinger在合成的時候會將一些合成工作委托給Hardware Compositer,降低GPU的負擔。合成好的數據放到屏幕對應的Frame Buffer中,屏幕依賴着自己的刷新頻率進行刷新,整個頁面就顯示在屏幕上了。