Android性能優化第(五)篇---Android UI渲染機制

藝術的渲染

優化性能一般從渲染,運算與內存,電量三個方面進行,今天開始說聊一聊Android的渲染機制,我們要知道Android系統每隔16ms就重新繪製一次Activity,也就是說,我們的應用必須在16ms內完成屏幕刷新的全部邏輯操作,即每一幀只能停留16ms,渲染機制說完之後,然後在說如何去優化UI。

1、爲什麼是16ms

16ms意味着1000/60hz,相當於60fps。這是因爲人眼與大腦之間的協作無法感知超過60fps的畫面更新。12fps大概類似手動快速翻動書籍的幀率, 這明顯是可以感知到不夠順滑的。24fps使得人眼感知的是連續線性的運動,這其實是歸功於運動模糊的效果。 24fps是電影膠圈通常使用的幀率,因爲這個幀率已經足夠支撐大部分電影畫面需要表達的內容,同時能夠最大的減少費用支出。 但是低於30fps是 無法順暢表現絢麗的畫面內容的,此時就需要用到60fps來達到想要的效果,超過60fps就沒有必要了。如果我們的應用沒有在16ms內完成屏幕刷新的全部邏輯操作,就會發生卡頓。**

2、爲什麼16ms沒完成繪製就會卡頓

Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,VSync是Vertical Synchronization(垂直同步)的縮寫,是一種在PC上很早就廣泛使用的技術,可以簡單的把它認爲是一種定時中斷。而在Android 4.1(JB)中已經開始引入VSync機制。

上圖所示是VSync機制下的繪製過程。從上圖可以看出,CPU和GPU的處理時間都少於一個VSync的間隔,即16.6ms。如果每個間隔都有繪製的情況下,當前的FPS即爲60幀。

當CPU和GPU處理時間都很慢,或因爲其他的原因,如在主線程中幹活太多,那麼就會出現如下圖這樣的狀況。


從上圖可以看到,CPU和GPU的處理時間因爲各種原因都大於一個VSync的間隔(16.6ms),所以在第二個VSync還在處理1區域的繪製時,不可能實現理論上的FPS60,同時也出現了丟幀(SF: Skipped Frame)情況。試想用戶盯着同一張圖看了32ms而不是16ms,當然很容易察覺出卡頓感,哪怕僅僅出現一次掉幀,用戶都會發現動畫不是很順暢,大家在察覺到APP卡頓的時候,可以看看logcat控制檯,會有drop frames類似的警告,那麼是什麼原因導致16ms沒能完成繪製的操作呢?

3、渲染原理

上面說了CPU和GPU的處理時間因爲各種原因都大於一個VSync的間隔(16.6ms),導致了卡頓。渲染操作通常依賴於兩個核心組件:CPU與GPU。CPU負責包括Measure,Layout,Record,Execute的計算操作,GPU 負責Rasterization(柵格化)操作。何爲柵格化,我也是第一次聽到這詞,看下圖。

柵格化
所謂的柵格化就是繪製那些Button,Shape,Path,String,Bitmap等組件最基礎的操作。它把那些組件拆分到不同的像素上進行顯示,說的俗氣一點,就是解決那些複雜的XML佈局文件和標記語言,使之轉化成用戶能看懂的圖像,但是這不是直接轉換的,XML佈局文件需要在CPU中首先轉換爲多邊形或者紋理,然後再傳遞給GPU進行格柵化,對於柵格化,跟OpenGL有關,格柵化是一個特別費時的操作。

分析到這裏,16毫秒的時間主要被兩件事情所佔用,第一件:將UI對象轉換爲一系列多邊形和紋理;第二件:CPU傳遞處理數據到GPU。所以很明顯,我們要縮短這兩部分的時間,也就是說需要儘量減少對象轉換的次數,以及上傳數據的次數,對否?

我們再看一圖,這圖簡單說明CPU和GPU的職責工作,以及可能發生的問題和解決方案。

列名 解釋
PIPELINE 管道
PROBLEM 發生的問題
TOOLS 用什麼工具來解決
SOLUTION 解決方案時什麼

在CPU方面,最常見的性能問題是不必要的佈局和失效,這些內容必須在視圖層次結構中進行測量、清除並重新創建,引發這種問題通常有兩個原因:一是重建顯示列表的次數太多,二是花費太多時間作廢視圖層次並進行不必要的重繪,這兩個原因在更新顯示列表或者其他緩存GPU資源時導致CPU工作過度。在GPU方面,最常見的問題是我們所說的過度繪製(overdraw),通常是在像素着色過程中,通過其他工具進行後期着色時浪費了GPU處理時間。下面我們對GPU和CPU產生的兩大問題進行優化。

  • CPU產生的問題:不必要的佈局和失效
  • GPU產生的問題:過度繪製(overdraw)
4、過度繪製(overdraw)*檢測

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

 按照以下步驟打開Show GPU Overrdraw的選項:設置 -> 開發者選項 -> 調試GPU過度繪製 -> 顯示GPU過度繪製

藍色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,
- 藍色: 意味着overdraw 1倍。像素繪製了兩次。大片的藍色還是可以接受的(若整個窗口是藍色的,可以擺脫一層)。
- 綠色: 意味着overdraw 2倍。像素繪製了三次。中等大小的綠色區域是可以接受的但你應該嘗試優化、減少它們。
- 淡紅: 意味着overdraw 3倍。像素繪製了四次,小範圍可以接受。
- 深紅: 意味着overdraw 4倍。像素繪製了五次或者更多。這是錯誤的,要修復它們。

我們的目標就是儘量減少紅色Overdraw,看到更多的藍色區域。

5、Overdraw 的處理方案
  • Overdraw 的處理方案一:去掉window的默認背景
    當我們使用了Android自帶的一些主題時,window會被默認添加一個純色的背景,這個背景是被DecorView持有的。當我們的自定義佈局時又添加了一張背景圖或者設置背景色,那麼DecorView的background此時對我們來說是無用的,但是它會產生一次Overdraw,帶來繪製性能損耗。去掉window的背景可以在onCreate()中setContentView()之後調用getWindow().setBackgroundDrawable(null);或者在theme中添加android:windowbackground=”null”;
  • Overdraw 的處理方案二:去掉其他不必要的背景
    有時候爲了方便會先給Layout設置一個整體的背景,再給子View設置背景,這裏也會造成重疊,如果子View寬度mach_parent,可以看到完全覆蓋了Layout的一部分,這裏就可以通過分別設置背景來減少重繪。再比如如果採用的是selector的背景,將normal狀態的color設置爲“@android:color/transparent”,也同樣可以解決問題。這裏只簡單舉兩個例子,我們在開發過程中的一些習慣性思維定式會帶來不經意的Overdraw,所以開發過程中我們爲某個View或者ViewGroup設置背景的時候,先思考下是否真的有必要,或者思考下這個背景能不能分段設置在子View上,而不是圖方便直接設置在根View上。
  • Overdraw 的處理方案三:clipRect的使用
    我們可以通過canvas.clipRect()來 幫助系統識別那些可見的區域。這個方法可以指定一塊矩形區域,只有在這個區域內纔會被繪製,其他的區域會被忽視。這個API可以很好的幫助那些有多組重疊 組件的自定義View來控制顯示的區域。同時clipRect方法還可以幫助節約CPU與GPU資源,在clipRect區域之外的繪製指令都不會被執行,那些部分內容在矩形區域內的組件,仍然會得到繪製。

  • Overdraw 的處理方案四:ViewStub
    ViewStub稱之爲“延遲化加載”,在教多數情況下,程序無需顯示ViewStub所指向的佈局文件,只有在特定的某些較少條件下,此時ViewStub所指向的佈局文件才需要被inflate,且此佈局文件直接將當前ViewStub替換掉,具體是通過viewStub.infalte()或viewStub.setVisibility(View.VISIBLE)來完成;

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ViewStub
        android:id="@+id/network_error_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/empty_view" />
</RelativeLayout>
 private void showNetError() {
        // not repeated infalte
        if (networkErrorView != null) {
            networkErrorView.setVisibility(View.VISIBLE);
            return;
        }

        ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
        networkErrorView = stub.inflate();
        Button networkSetting = (Button)networkErrorView.findViewById(R.id.network_setting);
        Button refresh = (Button)findViewById(R.id.network_refresh);
    }

    private void showNormal() {
        if (networkErrorView != null) {
            networkErrorView.setVisibility(View.GONE);
        }
    }
  • Overdraw 的處理方案五:Merge標籤
    MMerge標籤可以幹掉一個view層級。Merge的作用很明顯,但是也有一些使用條件的限制。有兩種情況下我們可以使用Merge標籤來做容器控件。第一種子視圖不需要指定任何針對父視圖的佈局屬性,就是說父容器僅僅是個容器,子視圖只需要直接添加到父視圖上用於顯示就行。另外一種是假如需要在LinearLayout裏面嵌入一個佈局(或者視圖),而恰恰這個佈局(或者視圖)的根節點也是LinearLayout,這樣就多了一層沒有用的嵌套,無疑這樣只會拖慢程序速度。而這個時候如果我們使用merge根標籤就可以避免那樣的問題。另外Merge只能作爲XML佈局的根標籤使用,當Inflate以開頭的佈局文件時,必須指定一個父ViewGroup,並且必須設定attachToRoot爲true。
6、減少不必要的層次:巧用Hierarchy Viewer

Hierarchy Viewer接觸過Android的人估計都用過,如果在真機上可以
使用ViewServer這個第三方庫:https://github.com/romainguy/ViewServer,配置步驟比較簡單,主要分爲如下三步:
第一步,在根build.gradle文件中加入

allprojects {
    repositories {
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

第二步,在Module的build.gradle文件中加入

dependencies {
    ...................................
    compile 'com.github.romainguy:ViewServer:017c01cd512cac3ec054d9eee05fc48c5a9d2de'
}

第三步,加上訪問網絡權限,在Activity添加下列代碼

public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        // Set content view, etc.  
        ViewServer.get(this).addWindow(this);  
    }  
  
    public void onDestroy() {  
        super.onDestroy();  
        ViewServer.get(this).removeWindow(this);  
    }  
  
    public void onResume() {  
        super.onResume();  
        ViewServer.get(this).setFocusedWindow(this);  
    }  

它只能在root過的機器才能使用,可以幫我們減少View的層,在Hierarchy Viewer窗口中,所有的子View上面都有了3個圈圈, (取色範圍爲紅、黃、綠色),這三個圈圈分別代表measure 、layout、draw的速度,並且你也可以看到實際的運行的速度,如果你發現某個View上的圈是紅色,那麼說明這個View相對其他的View,該操作運行最慢,注意只是相對別的View,並不是說就一定很慢。

佈局常見問題與優化建議
- 沒有用的父佈局時指沒有背景繪製或者沒有大小限制的父佈局,這樣的佈局不會對UI效果產生任何影響。我們可以把沒有用的父佈局,通過標籤合併來減少UI的層次;
- 使用線性佈局LinearLayout排版導致UI層次變深,如果有這類問題,我們就使用相對佈局RelativeLayout代替LinearLayout,減少UI的層次;
- 不常用的UI被設置成GONE,比如異常的錯誤頁面,如果有這類問題,我們需要用標籤,代替GONE提高UI性能。

參考鏈接:
http://www.cnblogs.com/krislight1105/p/5352517.html
http://www.csdn.net/article/2015-01-20/2823621-android-performance-patterns

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