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

一、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生命週期控制
  • 系統事件處理
  • 消息處理
  • 界面佈局
  • 界面繪製
  • 界面刷新

除了這些以外,儘量避免將其他處理放到主線程中,特別是複雜的數據計算和網絡請求。

 

 

聲明:本文參考《Android應用性能優化最佳實踐》一書。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章