1. 程式人生 > >Android進階——效能優化之佈局渲染原理和底層機制詳解(四)

Android進階——效能優化之佈局渲染原理和底層機制詳解(四)

引言

UI 全稱User Interaction,我第一次聽到這個名詞是在大學的時候,當時候上人機互動課,我們教授說他認為iPhone的i 就是代表Interaction的意思,暫且不必爭辯是非。回到我們軟體開發中來,UI是使用者感知與互動的第一且唯一的途徑,是影響使用者體驗最關鍵的一部分。而在Android中每一個寫在佈局中的元件都需要初始化,進行佈局,繪製的過程從而實現各種酷炫的UI效果,而實現UI的方式多種多樣,如果使用不當就會給使用者帶來不好的體驗,所以佈局優化在效能優化方向尤其重要,接下來將結合原始碼總結分享相關知識。效能優化系列文章連結:

一、CPU與GPU概述

眾所周知,目前在一般的計算系統內都會包含兩大重要的元件:CPU(中央處理器)和GPU(圖形處理器),現在還有一塊NPU。雖然他們僅僅一詞之差,但是由所應用的場景不同:CPU需要很強的通用性來處理各種不同的資料型別,同時又要進行復雜的數學和邏輯運算,這些都使得CPU的內部結構異常複雜;而GPU面對的則是型別高度統一的、相互無依賴的大規模資料和不需要被打斷的純淨的計算環境,自然結構也大不相同
這裡寫圖片描述


(來自nVidia CUDA文件)其中綠色的是計算單元(ALU),橙紅色的是儲存單元,橙黃色的是控制單元。GPU採用了數量眾多的計算單元和超長的流水線,但只有非常簡單的控制邏輯並省去了Cache。而CPU不僅被Cache佔據了大量空間,而且還有有複雜的控制邏輯和諸多優化電路,相比之下計算能力只是CPU很小的一部分。以前CPU除了做邏輯計算外,還負責記憶體管理、圖形顯示操作,因此在實際運算的時候效能會大打折扣,在沒有GPU的時代,不能顯示覆雜的圖形,其運算速度遠比不上今天覆雜三維遊戲的要求,即使CPU 的工作頻率超過2GHz或更高,對它繪製圖形提高也不大。於是GPU應運而生,GPU是將計算機系統所需要的顯示資訊進行轉換驅動,並向顯示器提供行掃描訊號,控制顯示器的正確顯示
,是連線顯示器和主機板的重要元件,簡而言之主要負責圖形顯示部分的工作,而CPU 只是負責把圖形繪製指令傳達給GPU,由GPU完成繪製後再反饋給CPU。比如說CPU想畫一個二維圖形,只需要發個指令給GPU,如“在座標位置(x, y)處畫個長和寬為a×b大小的長方形”,GPU就可以迅速計算出該圖形的所有畫素,並在顯示器上指定位置畫出相應的圖形,畫完後就通知CPU “我畫完了”,然後等待CPU發出下一條圖形指令。總而言之,在一個典型的顯示系統中通常包含CPU、GPU和顯示器三個部分, 其中CPU負責進行Measure、Layout、Record和Execute等計算操作,把計算好資料交給GPU;由GPU形資料進行渲染, Rasterization(柵格化)操作並進行驅動轉化;最後顯示器負責把buffer裡的資料呈現到
螢幕上。

這裡寫圖片描述

二、Android系統的繪圖機制

Android系統每隔16ms就重新繪製一次Activity,即要求應用必須在16ms內完成螢幕重新整理的全部邏輯操作,這樣才能達到每秒60幀,然而這個每秒幀數的引數由手機硬體所決定,現在大多數手機螢幕重新整理率是60赫茲(赫茲是國際單位制中頻率的單位,它是每秒中的週期性變動重複次數的計量),也就是說我們有16ms(1000ms/60次=16.66ms)的時間去完成每幀的繪製邏輯操作,如果超過了就會出現所謂的丟幀(比如說我們花費27ms才完成邏輯計算就可能造成丟幀)
這裡寫圖片描述
在Android應用層通過LayoutInflater把佈局XML檔案對映成物件載入到記憶體中,CPU經過運算處理成多維的向量圖形,然後交給GPU一個個畫素去填充。
這裡寫圖片描述

三、Android卡頓的底層的根源探究

相信不少面試的時候,都會被問過,卡頓的原因都有哪些?不少人回答UI層次結構過於複雜,巢狀不合理,過度繪製等,然後就沒有下文了,但是面試的時候有時候面試官出這個問題並不是想讓你告訴他這些理論,他更多的想聽你結合UI繪製渲染原理講下,甚至是底層機制講解下,這個問題我也曾經問過應聘者,然而很少人能回答得很深入,他給你的答案讓你沒法繼續深入,扯遠了,迴歸正題,Android 卡頓的真正根源在哪(其實這些答案android官網已經給出了詳細介紹,記住是英文官網,同樣的單詞我在中文官網查不到任何資訊,而在英文官網可以查到大概2.6W條記錄)我們將逐步解開謎底。

1、60Hz 和 16 ms

在瞭解這兩個名詞之前,先去熟悉一些非軟體專業的生物學的常識

  • 12 fps——由於人類眼睛的特殊生理結構,如果所看畫面之幀率高於每秒約10-12幀的時候,就會認為是連貫的
  • 24 fps——有聲電影的拍攝及播放幀率均為每秒24幀,對一般人而言已算可接受
  • 60 fps—— 在與手機互動過程中,如觸控和反饋60幀以下人是能感覺出來的。60幀以上不能察覺變化當幀率低於60 fps 時感覺的畫面的卡頓和遲滯現象

由於人體眼睛生理結構的特殊性,於是這就是60Hz的由來,而1000ms/60=16.66ms這就是16ms的由來。

2、SurfaceFlinger服務概述

SurfaceFlinger服務和其他系統服務一樣是在Android系統的System程序裡被啟動並執行在其中的,主要負責統一管理裝置中Android系統的幀緩衝區(Frame Buffer,簡單理解為螢幕所顯示出來的所有圖形效果都是由它統一管理的),在SurfaceFlinger服務啟動的過程中會自動建立兩個執行緒:其中一個執行緒用於監控控制檯事件;另外一個執行緒則用於渲染系統的UI,而Android應用程式為了能夠將自己的UI繪製在系統的幀緩衝區上,他就需要將UI資料傳遞(每一個Android應用程式與SurfaceFlinger服務之間,都會通過一塊匿名共享記憶體來傳遞UI資料)SurfaceFlinger服務並告知自己具體的UI資料(例如要繪製UI的區域、位置等資訊),那它就必須要與SurfaceFlinger服務進行通訊,但Android應用程式與SurfaceFlinger服務是執行在不同的程序中的,Android應用程式請求SurfaceFlinger服務渲染自己的UI可以分為三步曲:首先是建立一個到SurfaceFlinger服務的連線,接著再通過這個連線來建立一個Surface,最後請求SurfaceFlinger服務渲染該Surface(在Android應用的每個視窗對應一個畫布(Canvas),也可以理解為Android應用程式的一個視窗),它們之間通過Binder機制進行通訊,其實SurfaceFlinger服務本質上是一個Binder,至於細節方面不在這篇文章討論範圍內。因為SurfaceFlinger實在太複雜了,而且在APP層我們對於這部分的無法進行任何的優化,這是ROOM做的工作。

3、Android APP從開始構建UI到顯示在螢幕上背後的故事

簡而言之,Android應用程式呼叫SurfaceFlinger服務把經過測量、佈局和繪製後的Surface渲染到顯示螢幕上,這基本的流程按照各自的分工還可以分為:

3.1、APP的UI在應用層進行渲染

在Android應用程式窗口裡麵包含了很多檢視(View)元素,這些元素是以樹形結構來組織的構成最終的所謂檢視樹的結構:
這裡寫圖片描述
因此,在繪製一個Android應用程式視窗的UI之前,我們首先要確定它裡面的各個子View元素在父元素裡面的大小以及位置。確定各個子View元素在父View元素裡面的大小以及位置的過程又稱為測量過程和佈局過程。Android應用程式視窗的UI渲染過程可以分為Measure測量Layout佈局Draw繪製三個階段(由ViewRootImpl類的performTraversals()方法發起)

  • 測量——遞迴(深度優先)確定所有檢視的大小(高、寬)

  • 佈局——遞迴(深度優先)確定所有檢視的位置(左上角座標)

  • 繪製——在畫布canvas上繪製應用程式視窗所有的檢視

其中Android目前支援兩種繪製模型:基於軟體的繪製模型硬體加速的繪製模型(從Android 3.0開始)

  • 在基於軟體的繪製模型下,由CPU依次按照讓View層次結構失效、 繪製View層次結構兩個步驟主導繪圖。當應用程式需要更新它的部分UI時,都會自動呼叫內容發生改變的View物件的invalidate()方法(在View物件的屬性發生變化時,如背景色或TextView物件中的文字等,Android系統也會自動的呼叫該View物件的invalidate()方法)。無效(invalidation)訊息請求會在View物件層次結構中傳遞,以便計算出需要重繪的螢幕區域(髒區),接著Android系統會在View層次結構中繪製所有的跟髒區相交的區域。所以這種方式存在兩個顯著缺點:繪製了不需要重繪的檢視(與髒區域相交的區域)和掩蓋了一些應用的bug(由於會重繪與髒區域相交的區域)

  • 在基於硬體加速的繪製模式下,由GPU按照讓View層次結構失效記錄、更新顯示列表繪製顯示列表三個步驟主導繪圖,雖然Android系統依然會使用invalidate()方法和draw()方法來請求螢幕更新和展現View物件。但Android系統並不是立即執行繪製命令,而是首先把這些View的繪製函式作為繪製指令記錄一個顯示列表中,然後再讀取顯示列表中的繪製指令呼叫OpenGL相關函式完成實際繪製。另一個優化是,Android系統只需要針對由invalidate()方法呼叫所標記的View物件的髒區進行記錄和更新顯示列表。沒有失效的View物件則能重放先前顯示列表記錄的繪製指令來進行簡單的重繪工作。使用顯示列表的目的是,把檢視的各種繪製函式翻譯成繪製指令儲存起來,對於沒有發生改變的檢視把原先儲存的操作指令重新讀取出來重放一次就可以了,提高了檢視的顯示速度。而對於需要重繪的View,則更新顯示列表,以便下次重用,然後再呼叫OpenGL完成繪製。雖然硬體加速提高了Android系統顯示和重新整理的速度,但它有三個缺陷:相容性(部分繪製函式不支援或不完全硬體加速,見下圖)記憶體消耗(OpenGL API呼叫就會佔用8MB,而實際上會佔用更多記憶體)、 電量消耗(GPU耗電)

    這裡寫圖片描述

3.2、Android Framework層通過SurfaceFilnger服務把Surface上的UI資料渲染到硬體幀緩衝區中。

當Android應用層在圖形緩衝區中繪製好View層次結構後,應用層通過Binder機制與SurfaceFlinger通訊並藉助一塊匿名共享記憶體會把這個圖形緩衝區會被交給SurfaceFlinger服務。因為單純的匿名共享記憶體在傳遞多個視窗資料時缺乏有效的管理,所以匿名共享記憶體就被抽象為一個更上流的資料結構SharedClient,在每個SharedClient中,最多有31個SharedBufferStack,每個SharedBufferStack都對應一個Surface即一個視窗。

3.3 UI顯示重新整理機制和Vsync

Android系統每隔16ms發出Vsync訊號,觸發對UI 進行渲染(即每16ms顯示一幀),如果每次渲染都成功這樣就能夠達到流暢的畫面所需要的60fps,為了能夠實現60fps,這意味著計算渲染的大多數操作都必須在16ms內完成。一般我們在繪製UI的時候,都會採用一種稱為“雙緩衝”的技術。雙緩衝意味著要使用兩個緩衝區(SharedBufferStack中),其中一個稱為Front Buffer,另外一個稱為Back Buffer。UI總是先在Back Buffer中繪製,然後再和Front Buffer交換,渲染到顯示裝置中,其中Display處理前Front Buffer,CPU、GPU處理Back Buffer如下圖可以看出在16ms內需要完成兩項任務:將UI 物件轉換為一系列多邊形和紋理(柵格化)CPU傳遞處理資料到GPU
這裡寫圖片描述
在所有渲染任務都在16ms內完成的理想情況下,所有的UI顯示重新整理都很完美,但是假如時間從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),於是給人視覺上就造成了卡頓的感受,這根本原因是在第1個16ms段內,CPU/GPU沒有及時處理第2幀資料,可能是因為CPU在忙別的事情,不知道該到處理UI繪製的時間了,當CPU在想要去處理第2幀資料,時間又錯過了!於是在Android 4.1中引入了類似於時鐘中斷的Vsync機制,每收到Vsync訊號,CPU就開始處理各幀資料
這裡寫圖片描述
但這也是不是說就絕對的完美,假如在第二個16ms時間段,Display本應顯示B幀,但卻因為GPU還在處理B幀,導致A幀被重複顯示。 同理,在第二個16ms時間段內,CPU無所事事,因為A Buffer被Display在使用 而B Buffer被GPU在使用,一旦過了VSYNC時間點,CPU就不能被觸發以處理繪製工作了,可以類比到等高鐵的情況下,以上就是卡頓的基本原因了。一句話總結就是CPU和GPU的處理時間因為各種原因都大於一個VSync的間隔造成了該顯示下一幀的時候卻還是顯示上一幀,由於篇幅問題,下一篇再進行實戰優化總結。