版權聲明:本文為LooperJing原創文章,轉載請注明出處!
優化性能一般從渲染,運算與內存,電量三個方面進行,今天開始說聊一聊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效果產生任何影響。我們可以把沒有用的父布局,通過<merge/>標簽合并來減少UI的層次;
- 使用線性布局LinearLayout排版導致UI層次變深,如果有這類問題,我們就使用相對布局RelativeLayout代替LinearLayout,減少UI的層次;
- 不常用的UI被設置成GONE,比如異常的錯誤頁面,如果有這類問題,我們需要用<ViewStub/>標簽,代替GONE提高UI性能。
參考鏈接:
http://www.cnblogs.com/krislight1105/p/5352517.html
http://www.csdn.net/article/2015-01-20/2823621-android-performance-patterns