Android效能優化之渲染篇

胡凱發表於2015-09-16

Google近期在Udacity上釋出了Android效能優化的線上課程,分別從渲染,運算與記憶體,電量幾個方面介紹瞭如何去優化效能,這些課程是Google之前在Youtube上釋出的Android效能優化典範專題課程的細化與補充。

下面是渲染篇章的學習筆記,部分內容和前面的效能優化典範有重合,歡迎大家一起學習交流!

1)Why Rendering Performance Matters

現在有不少App為了達到很華麗的視覺效果,會需要在介面上層疊很多的檢視元件,但是這會很容易引起效能問題。如何平衡Design與Performance就很需要智慧了。

2)Defining ‘Jank’

大多數手機的螢幕重新整理頻率是60hz,如果在1000/60=16.67ms內沒有辦法把這一幀的任務執行完畢,就會發生丟幀的現象。丟幀越多,使用者感受到的卡頓情況就越嚴重。

3)Rendering Pipeline: Common Problems

渲染操作通常依賴於兩個核心元件:CPU與GPU。CPU負責包括Measure,Layout,Record,Execute的計算操作,GPU負責Rasterization(柵格化)操作。CPU通常存在的問題的原因是存在非必需的檢視元件,它不僅僅會帶來重複的計算操作,而且還會佔用額外的GPU資源。

4)Android UI and the GPU

瞭解Android是如何利用GPU進行畫面渲染有助於我們更好的理解效能問題。一個很直接的問題是:activity的畫面是如何繪製到螢幕上的?那些複雜的XML佈局檔案又是如何能夠被識別並繪製出來的?

Resterization柵格化是繪製那些Button,Shape,Path,String,Bitmap等元件最基礎的操作。它把那些元件拆分到不同的畫素上進行顯示。這是一個很費時的操作,GPU的引入就是為了加快柵格化的操作。

CPU負責把UI元件計算成Polygons,Texture紋理,然後交給GPU進行柵格化渲染。

然而每次從CPU轉移到GPU是一件很麻煩的事情,所幸的是OpenGL ES可以把那些需要渲染的紋理Hold在GPU Memory裡面,在下次需要渲染的時候直接進行操作。所以如果你更新了GPU所hold住的紋理內容,那麼之前儲存的狀態就丟失了。

在Android裡面那些由主題所提供的資源,例如Bitmaps,Drawables都是一起打包到統一的Texture紋理當中,然後再傳遞到GPU裡面,這意味著每次你需要使用這些資源的時候,都是直接從紋理裡面進行獲取渲染的。當然隨著UI元件的越來越豐富,有了更多演變的形態。例如顯示圖片的時候,需要先經過CPU的計算載入到記憶體中,然後傳遞給GPU進行渲染。文字的顯示比較複雜,需要先經過CPU換算成紋理,然後交給GPU進行渲染,返回到CPU繪製單個字元的時候,再重新引用經過GPU渲染的內容。動畫則存在一個更加複雜的操作流程。

為了能夠使得App流暢,我們需要在每幀16ms以內處理完所有的CPU與GPU的計算,繪製,渲染等等操作。

5)GPU Problem: Overdraw

Overdraw(過度繪製)描述的是螢幕上的某個畫素在同一幀的時間內被繪製了多次。在多層次重疊的UI結構裡面,如果不可見的UI也在做繪製的操作,會導致某些畫素區域被繪製了多次。這樣就會浪費大量的CPU以及GPU資源。

當設計上追求更華麗的視覺效果的時候,我們就容易陷入採用複雜的多層次重疊檢視來實現這種視覺效果的怪圈。這很容易導致大量的效能問題,為了獲得最佳的效能,我們必須儘量減少Overdraw的情況發生。

幸運的是,我們可以通過手機設定裡面的開發者選項,開啟Show GPU Overdraw的選項,觀察UI上的Overdraw情況。

藍色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,我們的目標就是儘量減少紅色Overdraw,看到更多的藍色區域。

6)Visualize and Fix Overdraw – Quiz & Solution

這裡舉了一個例子,通過XML檔案可以看到有好幾處非必需的background。通過把XML中非必需的background移除之後,可以顯著減少佈局的過度繪製。其中一個比較有意思的地方是:針對ListView中的Avatar ImageView的設定,在getView的程式碼裡面,判斷是否獲取到對應的Bitmap,在獲取到Avatar的影像之後,把ImageView的Background設定為Transparent,只有當影像沒有獲取到的時候才設定對應的Background佔點陣圖片,這樣可以避免因為給Avatar設定背景圖而導致的過度渲染。

總結一下,優化步驟如下:

移除Window預設的Background

移除XML佈局檔案中非必需的Background

按需顯示佔位背景圖片

7)ClipRect & QuickReject

前面有提到過,對不可見的UI元件進行繪製更新會導致Overdraw。例如Nav Drawer從前置可見的Activity滑出之後,如果還繼續繪製那些在Nav Drawer裡面不可見的UI元件,這就導致了Overdraw。為了解決這個問題,Android系統會通過避免繪製那些完全不可見的元件來儘量減少Overdraw。那些Nav Drawer裡面不可見的View就不會被執行浪費資源。

但是不幸的是,對於那些過於複雜的自定義的View(通常重寫了onDraw方法),Android系統無法檢測在onDraw裡面具體會執行什麼操作,系統無法監控並自動優化,也就無法避免Overdraw了。但是我們可以通過canvas.clipRect()來幫助系統識別那些可見的區域。這個方法可以指定一塊矩形區域,只有在這個區域內才會被繪製,其他的區域會被忽視。這個API可以很好的幫助那些有多組重疊元件的自定義View來控制顯示的區域。同時clipRect方法還可以幫助節約CPU與GPU資源,在clipRect區域之外的繪製指令都不會被執行,那些部分內容在矩形區域內的元件,仍然會得到繪製。

除了clipRect方法之外,我們還可以使用canvas.quickreject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形區域內的繪製操作。

8)Apply clipRect and quickReject – Quiz & Solution

上面的示例圖中顯示了一個自定義的View,主要效果是呈現多張重疊的卡片。這個View的onDraw方法如下圖所示:

開啟開發者選項中的顯示過度渲染,可以看到我們這個自定義的View部分割槽域存在著過度繪製。那麼是什麼原因導致過度繪製的呢?

9)Fixing Overdraw with Canvas API

下面的程式碼顯示瞭如何通過clipRect來解決自定義View的過度繪製,提高自定義View的繪製效能:

下面是優化過後的效果:

10)Layouts, Invalidations and Perf

Android需要把XML佈局檔案轉換成GPU能夠識別並繪製的物件。這個操作是在DisplayList的幫助下完成的。DisplayList持有所有將要交給GPU繪製到螢幕上的資料資訊。

在某個View第一次需要被渲染時,Display List會因此被建立,當這個View要顯示到螢幕上時,我們會執行GPU的繪製指令來進行渲染。

如果View的Property屬性發生了改變(例如移動位置),我們就僅僅需要Execute Display List就夠了。

然而如果你修改了View中的某些可見元件的內容,那麼之前的DisplayList就無法繼續使用了,我們需要重新建立一個DisplayList並重新執行渲染指令更新到螢幕上。

請注意:任何時候View中的繪製內容發生變化時,都會需要重新建立DisplayList,渲染DisplayList,更新到螢幕上等一系列操作。這個流程的表現效能取決於你的View的複雜程度,View的狀態變化以及渲染管道的執行效能。舉個例子,假設某個Button的大小需要增大到目前的兩倍,在增大Button大小之前,需要通過父View重新計算並擺放其他子View的位置。修改View的大小會觸發整個HierarcyView的重新計算大小的操作。如果是修改View的位置則會觸發HierarchView重新計算其他View的位置。如果佈局很複雜,這就會很容易導致嚴重的效能問題。

11)Hierarchy Viewer: Walkthrough

Hierarchy Viewer可以很直接的呈現佈局的層次關係,檢視元件的各種屬性。 我們可以通過紅,黃,綠三種不同的顏色來區分佈局的Measure,Layout,Executive的相對效能表現如何。

12)Nested Hierarchies and Performance

提升佈局效能的關鍵點是儘量保持佈局層級的扁平化,避免出現重複的巢狀佈局。例如下面的例子,有2行顯示相同內容的檢視,分別用兩種不同的寫法來實現,他們有著不同的層級。

下圖顯示了使用2種不同的寫法,在Hierarchy Viewer上呈現出來的效能測試差異:

13)Optimizing Your Layout

下圖舉例演示瞭如何優化ListItem的佈局,通過RelativeLayout替代舊方案中的巢狀LinearLayout來優化佈局。

相關文章