轉載一篇詳細的分析:Android View繪製和顯示原理簡介
圖片沒有貼上過來,還是去原連結看吧。
現在越來越多的應用開始重視流暢度
方面的測試,瞭解Android應用程式是如何在螢幕上顯示的則是基礎中的基礎,就讓我們一起看看小小螢幕中大大的學問。這也是下篇文章——《Android應用流暢度測試分析》的基礎。
首先,用一句話來概括一下Android應用程式顯示的過程:Android應用程式呼叫SurfaceFlinger服務把經過測量、佈局和繪製後的Surface渲染到顯示螢幕上。
名詞解釋
SurfaceFlinger:Android系統服務,負責管理Android系統的幀緩衝區,即顯示螢幕。
Surface:Android應用的每個視窗對應一個畫布(Canvas),即Surface,可以理解為Android應用程式的一個視窗。
Android應用程式的顯示過程包含了兩個部分(應用側繪製、系統側渲染)、兩個機制(程序間通訊機制、顯示重新整理機制),接下來我們就來一一道來。
應用側
一個Android應用程式窗口裡麵包含了很多UI元素,這些UI元素是以樹形結構來組織的,即它們存在著父子關係,其中,子UI元素位於父UI元素裡面,如下圖:
因此,在繪製一個Android應用程式視窗的UI之前,我們首先要確定它裡面的各個子UI元素在父UI元素裡面的大小以及位置。確定各個子UI元素在父UI元素裡面的大小以及位置的過程又稱為測量過程和佈局過程。因此,Android應用程式視窗的UI渲染過程可以分為測量、佈局和繪製三個階段。如下圖所示:
測量:遞迴(深度優先)確定所有檢視的大小(高、寬)
佈局:遞迴(深度優先)確定所有檢視的位置(左上角座標)
繪製:在畫布canvas上繪製應用程式視窗所有的檢視
測量、佈局沒有太多要說的,這裡要著重說一下繪製。Android目前有兩種繪製模型:基於軟體的繪製模型和硬體加速的繪製模型(從Android 3.0開始全面支援)。
在基於軟體的繪製模型下,CPU主導繪圖,檢視按照兩個步驟繪製:
讓View層次結構失效
繪製View層次結構
當應用程式需要更新它的部分UI時,都會呼叫內容發生改變的View物件的invalidate()方法。無效(invalidation)訊息請求會在View物件層次結構中傳遞,以便計算出需要重繪的螢幕區域(髒區)。然後,Android系統會在View層次結構中繪製所有的跟髒區相交的區域。不幸的是,這種方法有兩個缺點:
繪製了不需要重繪的檢視(與髒區域相交的區域)
掩蓋了一些應用的bug(由於會重繪與髒區域相交的區域)
注意:在View物件的屬性發生變化時,如背景色或TextView物件中的文字等,Android系統會自動的呼叫該View物件的invalidate()方法。
在基於硬體加速的繪製模式下,GPU主導繪圖,繪製按照三個步驟繪製:
讓View層次結構失效
記錄、更新顯示列表
繪製顯示列表
這種模式下,Android系統依然會使用invalidate()方法和draw()方法來請求螢幕更新和展現View物件。但Android系統並不是立即執行繪製命令,而是首先把這些View的繪製函式作為繪製指令記錄一個顯示列表中,然後再讀取顯示列表中的繪製指令呼叫OpenGL相關函式完成實際繪製。另一個優化是,Android系統只需要針對由invalidate()方法呼叫所標記的View物件的髒區進行記錄和更新顯示列表。沒有失效的View物件則能重放先前顯示列表記錄的繪製指令來進行簡單的重繪工作。
使用顯示列表的目的是,把檢視的各種繪製函式翻譯成繪製指令儲存起來,對於沒有發生改變的檢視把原先儲存的操作指令重新讀取出來重放一次就可以了,提高了檢視的顯示速度。而對於需要重繪的View,則更新顯示列表,以便下次重用,然後再呼叫OpenGL完成繪製。
硬體加速提高了Android系統顯示和重新整理的速度,但它也不是萬能的,它有三個缺陷:
相容性(部分繪製函式不支援或不完全硬體加速,參見文章尾)
記憶體消耗(OpenGL API呼叫就會佔用8MB,而實際上會佔用更多記憶體)
電量消耗(GPU耗電)
系統側
Android應用程式在圖形緩衝區中繪製好View層次結構後,這個圖形緩衝區會被交給SurfaceFlinger服務,而SurfaceFlinger服務再使用OpenGL圖形庫API來將這個圖形緩衝區渲染到硬體幀緩衝區中。
由於Android應用程式很少能涉及到Android系統底層,所以SurfaceFlinger服務的執行過程不做過多的介紹。
程序間通訊機制
Android應用程式為了能夠將自己的UI繪製在系統的幀緩衝區上,它們就必須要與SurfaceFlinger服務進行通訊,如圖所示:
Android應用程式與SurfaceFlinger服務是執行在不同的程序中的,因此,它們採用某種程序間通訊機制來進行通訊。由於Android應用程式在通知SurfaceFlinger服務來繪製自己的UI的時候,需要將UI資料傳遞給SurfaceFlinger服務,例如,要繪製UI的區域、位置等資訊。一個Android應用程式可能會有很多個視窗,而每一個視窗都有自己的UI資料,因此,Android系統的匿名共享記憶體機制就派上用場了。
每一個Android應用程式與SurfaceFlinger服務之間,都會通過一塊匿名共享記憶體來傳遞UI資料,如下所示:
但是單純的匿名共享記憶體在傳遞多個視窗資料時缺乏有效的管理,所以匿名共享記憶體就被抽象為一個更上流的資料結構SharedClient,如下圖所示:
在每個SharedClient中,最多有31個SharedBufferStack,每個SharedBufferStack都對應一個Surface,即一個視窗。這樣,我們就可以知道為什麼每一個SharedClient裡面包含的是一系列SharedBufferStack而不是單個SharedBufferStack:一個SharedClient對應一個Android應用程式,而一個Android應用程式可能包含有多個視窗,即Surface。從這裡也可以看出,一個Android應用程式至多可以包含31個視窗。
每個SharedBufferStack中又包含了N個緩衝區(<4.1 N=2; >=4.1 N=3),即顯示重新整理機制中即將提到的雙緩衝和三重緩衝技術。
顯示重新整理機制
一般我們在繪製UI的時候,都會採用一種稱為“雙緩衝”的技術。雙緩衝意味著要使用兩個緩衝區(SharedBufferStack中),其中一個稱為Front Buffer,另外一個稱為Back Buffer。UI總是先在Back Buffer中繪製,然後再和Front Buffer交換,渲染到顯示裝置中。理想情況下,這樣一個重新整理會在16ms內完成(60FPS),下圖就是描述的這樣一個重新整理過程(Display處理前Front Buffer,CPU、GPU處理Back Buffer。
但現實情況並非這麼理想。
時間從0開始,進入第一個16ms:Display顯示第0幀,CPU處理完第一幀後,GPU緊接其後處理繼續第一幀。三者互不干擾,一切正常。
時間進入第二個16ms:因為早在上一個16ms時間內,第1幀已經由CPU,GPU處理完畢。故Display可以直接顯示第1幀。顯示沒有問題。但在本16ms期間,CPU和GPU卻並未及時去繪製第2幀資料(注意前面的空白區),而是在本週期快結束時,CPU/GPU才去處理第2幀資料。
時間進入第3個16ms,此時Display應該顯示第2幀資料,但由於CPU和GPU還沒有處理完第2幀資料,故Display只能繼續顯示第一幀的資料,結果使得第1幀多畫了一次(對應時間段上標註了一個Jank)。
通過上述分析可知,此處發生Jank的關鍵問題在於,為何第1個16ms段內,CPU/GPU沒有及時處理第2幀資料?原因很簡單,CPU可能是在忙別的事情,不知道該到處理UI繪製的時間了。可CPU一旦想起來要去處理第2幀資料,時間又錯過了!
為解決這個問題,Android 4.1中引入了VSYNC,這類似於時鐘中斷。結果如下圖所示:
由上圖可知,每收到VSYNC中斷,CPU就開始處理各幀資料。整個過程非常完美。
不過,仔細琢磨後卻會發現一個新問題:上圖中,CPU和GPU處理資料的速度似乎都能在16ms內完成,而且還有時間空餘,也就是說,CPU/GPU的FPS(幀率,Frames Per Second)要高於Display的FPS。確實如此。由於CPU/GPU只在收到VSYNC時才開始資料處理,故它們的FPS被拉低到與Display的FPS相同。但這種處理並沒有什麼問題,因為Android裝置的Display FPS一般是60,其對應的顯示效果非常平滑。
如果CPU/GPU的FPS小於Display的FPS,會是什麼情況呢?請看下圖:
由上圖可知:
在第二個16ms時間段,Display本應顯示B幀,但卻因為GPU還在處理B幀,導致A幀被重複顯示。
同理,在第二個16ms時間段內,CPU無所事事,因為A Buffer被Display在使用。B Buffer被GPU在使用。注意,一旦過了VSYNC時間點,CPU就不能被觸發以處理繪製工作了。
為什麼CPU不能在第二個16ms處開始繪製工作呢?原因就是隻有兩個Buffer(Android 4.1之前)。如果有第三個Buffer的存在,CPU就能直接使用它,而不至於空閒。出於這一思路就引出了三重緩衝區(Android 4.1)。結果如下圖所示:
由上圖可知:
第二個16ms時間段,CPU使用C Buffer繪圖。雖然還是會多顯示A幀一次,但後續顯示就比較順暢了。
是不是Buffer越多越好呢?回答是否定的。由上圖可知,在第二個時間段內,CPU繪製的第C幀資料要到第四個16ms才能顯示,這比雙Buffer情況多了16ms延遲。所以,Buffer最好還是兩個,三個足矣。
到這裡,Android系統的顯示原理就介紹完了。那麼在瞭解這些原理後對我們的流暢度測試有哪些幫助呢,請看我的下篇文章《Android應用流暢度測試分析》。
附:不同的API Level下,繪製函式對硬體加速模式的支援情況