Android優化——繪製優化之android系統顯示原理(一)

李桐桐發表於2018-10-19

一、android系統顯示原理

可以簡單概括為:android應用程式把經過測量、佈局、繪製後的surface快取資料,通過SurfaceFlinger把資料渲染到顯示螢幕上,通過android的重新整理機制來重新整理資料。也就是說應用層負責繪製,系統層負責渲染,通過程式間通訊把應用層需要繪製的資料傳遞到系統層服務,系統層服務通過重新整理機制把資料更新到螢幕。

android的圖形顯示系統採用的是Client/Server架構。SurfaceFlinger(Server)由C++程式碼編寫。Client端程式碼分為兩部分,一部分由java提供給應用層使用的API,另一部分則是由C++寫成的底層具體實現。

1、基本概念

CPU: 中央處理器,它整合了運算、緩衝、控制等單元,包括繪圖功能。CPU將物件處理為多維圖形,紋理(Bitmaps、Drawables等都是一起打包到統一的Texture紋理)。

GPU:一個類似於CPU的專門用來處理Graphics的處理器, 作用用來幫助加快格柵化操作,當然,也有相應的快取資料(例如快取已經光柵化過的bitmap等)機制。

DisplayList:它相當於是從View的繪製命令到GL命令之間的“中間語言”。它記錄了繪製該View所需的全部資訊,之後只要重放(replay)即可完成內容的繪製。這樣如果View沒有改動或只部分改動,便可重用或修改DisplayList,從而避免呼叫了一些上層程式碼,提高了效率。

柵格化:是將圖片等向量資源,轉化為一格格畫素點的畫素圖,顯示到螢幕上。

FPS(Frames Per Second):表示每秒傳遞的幀數。通俗來講就是指動畫或視訊的畫面數,對應的就是APP UI介面的刷行頻率,在一個UI動畫的播放過程中,FPS越大,介面表現越流暢,FPS越低,介面表現越卡頓。

2、繪製原理

2.1 應用層

在android的每個view繪製中有三個核心步驟:通過Measure和Layout來確定當前需要繪製的view所在的大小和位置,通過繪製(draw)到surface,在android系統中整體繪圖原始碼是在ViewRootImp類的performTraversals()方法,通過這個方法可以看出Measuret Layout都是遞迴來獲取view的大小和位置,並且以深度作為優先順序。由此可以看出,層級越深,元素越多,耗時也就越長。View繪製流程為:Measure-->Layout-->Draw。

2.1.1、Measure

用深度優先原則遞迴得到所有檢視的寬、高;獲取當前View的正確寬度childWidthMeasureSpec和高度childHeightMeasureSpec之後,可以呼叫它的成員函式Measure來設定它的大小。如果當前正在測量的檢視是一個容器,那麼它又會重複執行操作,直到它的所有子孫檢視大小都測量完成為止。

2.1.2、Layout

用深度優先原則遞迴得到所有檢視的位置;當前一個子view在應用程式視窗左上角的位置確定後,再結合它在前面測量得到的寬度和高度,就可以完全確定他在應用程式視窗中的佈局。

2.1.3、Draw

分為兩種繪製方式:軟體繪製(CPU)和硬體加速(GPU),其中硬體加速在android3.0開始已經全面支援,很明顯,硬體加速在UI的顯示及繪製上效率遠高於CPU繪製,但也有一些缺點:

  • 耗電:GPU的功耗比CPU高。

  • 相容問題:某些介面和函式不支援硬體加速。

  • 記憶體大:使用OPenGL的介面至少需要8MB記憶體。

2.2系統層

2.2.1 SurfaceFlinger服務

真正把需要顯示的資料渲染到螢幕上,是通過系統級程式中的SurfaceFlinger服務來實現的。它的主要工作有:

  • 響應客戶端事件,建立Layer與客戶端的Surface建立連線
  • 接收客戶端資料及屬性,修改Layer屬性,如尺寸、顏色、透明度等。
  • 將建立的Layer內容重新整理到螢幕上。
  • 維持Layer的序列,並對Layer最終輸出做出裁剪計算。

在android的顯示系統中使用了android的匿名共享記憶體:SharedClient,來實現跨程式的資料傳輸。

1、每個應用和SurfaceFlinger之間都會建立一個SharedClient,一個應用對應一個SharedClient。

2、SharedClient包含的是SharedBufferStack的集合,每個SharedClient中最多建立31個SharedBufferStack。

3、每個SharedBufferStack都對應一個Surface,也就是一個Window,這意味著一個android應用程式最多可以包含31個視窗。

4、每個SharedBufferStack中包含兩個(低於4.1版本)或者三個(4.1及以上版本)緩衝區,即後面顯示重新整理機制中提到的雙緩衝和三重緩衝技術。

最後總起來顯示整體流程分三個模組:應用層繪製到快取區;SurfaceFlinger把快取區資料渲染到螢幕;由於是兩個不同的程式,所以使用android的匿名共享記憶體SharedClient快取需要顯示的資料來達到目的。

繪製過程首先是CPU準備資料,通過Driver層把資料交給GPU渲染,其中CPU負責Measure、Layout、Record、Execute的資料計算工作,GPU負責柵格化、渲染。由於圖形API不允許CPU直接與GPU通訊,而是通過中間的一個圖形驅動層(Graphics Driver)來連線兩部分。圖形驅動維護了一個佇列,CPU把DisplayList新增到佇列中,GPU從這個佇列取出資料進行繪製,最終才在螢幕上顯示出來。

2.2.2 60Hz 和 16 ms

12 FPS——由於人類眼睛的特殊生理結構,如果所看畫面之幀率高於每秒約10-12幀的時候,就會認為是連貫的。

24 FPS——有聲電影的拍攝及播放幀率均為每秒24幀,對一般人而言已算可接受。

60 FPS—— 在與手機互動過程中,如觸控和反饋60幀以下人是能感覺出來的。60幀以上不能察覺變化,當幀率低於60FPS 時感覺的畫面的卡頓和遲滯現象。

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

3、重新整理機制

Android系統每隔16ms發出VSync訊號,觸發對UI進行渲染(即每16ms顯示一幀),如果每次渲染都成功這樣就能夠達到流暢的畫面所需要的60fps,為了能夠實現60fps,這意味著計算渲染的大多數操作都必須在16ms內完成。如果某個操作花費時間是24ms,系統在得到VSync訊號時就無法進行正常渲染,這樣就發生了丟幀現象。那麼使用者在32ms內看到的會是同一幅畫面,從而感覺卡頓。有很多原因可以導致CPU或者GUP負載過重從而出現丟幀現象:可能是Layout太過複雜,無法在16ms內完成渲染;可能是UI上有層疊太多的繪製單元;還有可能是動畫執行次數過多。

在android4.1版本中有效處理了UI流暢性差的問題。其解決方法即在4.1版本推出的Project Buffer。Project Buffer對android Display系統進行了重構,引入三個核心元素:VSync、Triple Buffer和Choreographer。其中VSync是理解Project Buffer的核心,,簡單地可以把它認為是一種定時中斷技術。Choreographer起除錯的作用,將繪製工作統一到VSync的某個時間點上,使應用的繪製工作有序。

雙緩衝:顯示內容的資料記憶體。我們知道在Linux上通常使用Framebuffer來做顯示輸出,當使用者程式更新Framebuffer中的資料後,顯示驅動會把Framebuffer中每個畫素點的值更新到螢幕,但是這樣會有一個問題,如果上一幀資料還沒顯示完,Framebuffer中的資料又更新了,就會帶來殘影問題,給使用者的直觀感覺就會有閃爍感,所以普遍採用了雙緩衝技術。雙緩衝意味著要使用兩個緩衝區(在SharedBufferStack中),其中一個稱為Front Buffer,另一個稱為Back Buffer。UI總是先在Back Buffer中繪製,然後再和Front Buffer交換,渲染到顯示裝置中。即只有當另一個buffer的資料準備好後,通過io_ctrl來通知顯示裝置切換buffer。

VSync(Verical Synchronization):垂直同步,從前面的雙緩衝介紹中可以瞭解到,只有當另一個buffer準備好後,才能通知重新整理 ,這就需要CPU以主動查詢的方式來保證資料是否準備好,因為這種機制效率很低,所以引入了VSync。可以簡單地把它認為是一種定時中斷,一旦收到VSync中斷,CPU就開始處理各幀資料。 Choreographer:收到VSync訊號時,呼叫 使用者設定的回撥函式。一共有以下三種型別的回撥:

  • CALLBACK_INPUT:優先順序最高,與輸入事件有關。

  • CALLBACK_ANIMATION:第二優先順序,與動畫有關。

  • CALLBACK_TRAVERSAL:最低優先順序,與UI控制元件繪製有關。

接下來通過時序圖來分析重新整理的過程,這些時序圖是2018年Google I/O講解新的顯示系統提供的,圖3.1所示的時序圖有三個元素:Display(顯示裝置),CPU-CPU準備資料,GPU-GPU準備資料。最下面的顯示時間,根據理想的60FPS,以16ms為一個顯示週期。

圖3.1    沒有Vsync資訊的重新整理

(1)沒有VSnyc訊號同步

我們以16ms為單位來進行分析:

1)從第一16ms開始看,Display顯示第0幀,CPU處理完第一幀後GPU緊接其後處理第一幀。三者都在正常工作。

2)時間進入第二個16ms:因為在上一個16ms時間內,第1幀已經由CPU和GPU處理完畢。所以Display可以正常顯示第1幀。顯示沒有問題,但在本16ms期間,CPU和GPU並未及時繪製第2幀資料(前面的空白區在忙別的事情),而是在本週期快結束時,CPU/GPU才去處理第2幀資料。

3)時間進入第3個16ms,此時Display應該顯示第2幀資料,但由於CPU和GPU還沒有處理完第2幀資料,故Display只能繼續顯示第1幀的資料,結果使得第1幀多畫了一次(對應時間段上標註了一個Jank),這就導致錯過了顯示第2幀。

通過上述分析可知,在第二個16ms時,發生Jank的關鍵問題在於,為何在第1個16ms段內,CPU/GPU沒有及時處理第2幀資料?從第2個16ms開始有一段空白的時間,可以說明原因所在,那就是CPU可能是在忙別的事情 ,不知道該到處理UI繪製的時間了。可CPU一旦想起來要去處理第2幀資料,時間又錯過了。為解決這個問題,4.1版本推出了Project Buffer,核心目的就是解決重新整理不同步的問題。

(2)有VSync訊號同步

加入VSync後,從圖3.2可以看到,一旦收到VSync中斷,CPU就開始處理各幀的資料。大部分的android顯示裝置重新整理率是60Hz,這也就意味著第一幀最多隻能有1/60=16ms左右的準備時間。假如CPU/GPU的FPS高於這個值,顯示效果將更好。但是,這時又出現一個新問題:CPU和GPU處理資料的速度都能在16ms內完成,而且還有時間空餘,但必須等到VSync訊號到來後,才處理下一幀資料,因此CPU/GPU的FPS被拉低到與Display的FPS相同。

從圖3.3採用雙緩衝區的顯示效果來看:在雙緩衝下,CPU/GPU的FPS大於重新整理頻率同時採用了雙緩衝技術以及VSync,可以看到整個過程還是相當不錯的,雖然CPU/GPU處理所用的時間時短時長,但總體來說都在16ms內,因而不影響顯示效果。A和B分別代表兩個緩衝區,它們不斷交換來正確顯示畫面。但如果CPU/GPU的FPS小於DIsplay的FPS,情況又不同了,如圖3.4所示。

圖3.2    有VSnyc的繪製

圖3.3    雙緩衝下的時序圖

圖3.4    雙緩衝下CPU/GPU的FPS小於重新整理頻率的時序圖

從圖3.4可以看到,當CPU/GPU的處理時間超過16ms時,第一個VSync就已經到來,但緩衝區B中的資料卻還沒有準備好,這樣就只能繼續顯示之前A緩衝區中的內容。而後面B完成後,又因為還沒有VSync訊號,CPU/GPU這個時候只能等待下一個VSync的來臨才開始處理下一幀資料。因此在整個過程中,有一大段時間被浪費。總結這段話就是:

1)在第2個16ms時間段內,Display本就顯示B幀,但因為GPU還在處理B幀,導致A幀被重複顯示。

2)同理,在第動起來個16ms時間段內,CPU無所事事,因為A Buffer由Display的使用。B Buffer由GPU使用。注意,一旦過了VSync時間點,CPU就不能被觸發以及處理繪製工作了。

為什麼CPU不能在第2個16ms時間處即VSync到來就開始工作呢?很明顯,原因就是隻有兩個Buffer。如果有第三個Buffer存在,CPU就可以開始工作,而不至於空閒。於是在android4.1以後,引出了第三個緩衝區:Triple Buffer。Triple Buffer利用CPU/GPU的空閒等待時間提前準備好資料,並不一定會使用。

引入Triple Buffer後的重新整理時序如圖3.5所示。

圖3.5    使用Triple Buffer時序圖

在第二個16ms時間段,CPU使用C Buffer繪圖。雖然還是會多顯示一次A幀,但後續顯示就比較順暢了。是不是Buffer越多越好呢?回答是否定的。由圖3.5可知,在第二個時間段內,CPU繪製的和C幀資料要到第四個16ms才顯示,這比雙快取情況多了16ms延遲。所以緩衝區不是越多越好,要做到平衡到最佳效果。

從以上分析來看,andorid系每戶在顯示機制上解決了android UI顯示不流暢的問題,並且從Google 2012年I/O大會給出的視訊來看,其效果也達到了預期。但實際在應用開發過程中仍然存在卡頓的現象。因為VSync中斷處理的執行緒優先順序一定要最高,否則即使接收到VSync中斷,不能及時處理,也是徒勞無功。

4、卡頓的根本原因

那卡頓的根本原因是什麼呢,從android系統的顯示原理中可以看到,影響繪製的根本原因有以下兩方面:

繪製任務太重,繪製一幀內容耗時太長。 主執行緒太忙了,導致VSync訊號來時還沒有準備好資料導致丟幀。 耗時太長,需要從UI佈局和繪製上來具體分析。這裡主要討論下第二個方面。我們知道所有的繪製工作都是由主執行緒,也就是UI執行緒來負責,主執行緒的關鍵職責是處理使用者互動,在螢幕上繪製畫素,並進行載入顯示相關的資料。在android應用開發中 ,特別需要避免任何阻礙主執行緒的事情,這樣應用程式才能保持對使用者操作的即時響應。

在實際的開發過程中,我們需要知道主執行緒應該做什麼,總結起來主執行緒主要做以下幾個方面工作:

  • UI生命週期控制
  • 系統事件處理
  • 訊息處理
  • 介面佈局
  • 介面繪製
  • 介面重新整理 除了這些以外,儘量避免將其他處理放到主執行緒中,特別是複雜的資料計算和網路請求。

相關文章