Android性能優化方面也有很多文章了,這里就做一個總結,從原理到方法,工具等做一個簡單的了解,從而可以慢慢地改變編碼風格,從而提高性能。
一、Android系統是如何處理UI組件的更新操作的
既然和布局相關,那么我們需要了解Android系統是如何處理UI組件的更新操作的。
1、Android需要把XML布局文件轉換成GPU能夠識別並繪制的對象。這個操作是在DisplayList的幫助下完成的。DisplayList持有所有將要交給GPU繪制到屏幕上的數據信息。
2、CPU負責把UI組件計算成Polygons,Texture紋理,然后交給GPU進行柵格化渲染。
3、GPU進行柵格化渲染。
4、硬件展示在屏幕上。
那么什么是柵格化呢?Resterization柵格化是繪制那些Button,Shape,Path,String,Bitmap等組件最基礎的操作,它把那些組件拆分到不同的像素上進行顯示,如上圖所示,手機的屏幕其實都是有一個一個小格子組成的圖片,而當這些小格子非常非常小的時候,那么就看起來像上圖第一個@中所示一樣了。這是一個很費時的操作,因此這也是為什么要引入GPU原因。
每次從CPU轉移到GPU是一件很麻煩的事情,所幸的是OpenGL ES可以把那些需要渲染的文理Hold在GPU的Memory里面,在下次需要渲染的時候直接進行操作。Android里面那些由主題所提供的資源,例如Bitmap,Drawables都是一起打包到統一的Texture文理當中, 然后再傳遞到GPU里面,這意味着每次你需要使用這些資源的時候,都是直接從文理里面進行獲取渲染的。當然隨着UI組件的越來越豐富,有了更多演變的形態。例如顯示圖片的時候,需要先經過CPU的計算加載到內存中,然后傳遞到GPU進行渲染。文字的顯示更加復雜,需要先經過CPU換算成紋理,然后再交給GPU進行渲染,回到CPU繪制單個字符的時候,再重新引用經過GPU渲染的內容。動畫則是一個更加復雜的流程。
為了能夠使得APP流暢,我們需要在每一幀16ms以內完成所有的CPU與GPU計算,繪制,渲染等等操作。也就是幀率為60fps,為什么幀率要為60fps呢,因為人眼與大腦之間的協作無法感知超過60fps的畫面更新。開發app的性能目標就是保持60fps,這意味着每一幀你只有16ms=1000/60的時間來處理所有的任務。這里需要了解下刷新率和幀率:
Refresh Rate:代表了屏幕在一秒內刷新屏幕的次數,這取決於硬件的固定參數,例如60HZ。
Frame Rate:代表了GPU在一秒內揮之操作的幀數,例如30fps,60fps。
此外這里引入了VSYNC的機制,Android就是通過VSYNC信號來同步UI繪制和動畫,使得它們可以獲得一個達到60fps的固定的幀率。GPU會獲取圖形數據進行渲染,然后硬件負責把渲染后的內容呈現到屏幕上,他們兩者不停地進行協作。不幸的是,刷新頻率和幀率並不是總能保持相同的節奏。如果發生幀率與刷新頻率不一致的情況,就會容易出現顯示內容發生斷裂,重疊。
幀率超過刷新率只是理想的狀況,在超過60fps的情況下,GPU所產生的幀數據會因為等待VSYNC的刷新信息而被Hold住,這樣能夠保持每次刷新都有實際的新的數據可以顯示。

但是我們遇到更多的情況是幀率小於刷新頻率。在這種情況下,某些幀顯示的畫面內容就會與上一幀的畫面相同。糟糕的事情是,幀率從超過60fps突然掉到60fps以下,這樣就會發生LAG,JANK,HITCHING等卡頓停頓的不順滑的情況,這也是用戶感受不好的原因所在。
在某個View第一次需要被渲染時,DisplayList會因此而被創建,當這個View要顯示到屏幕上時,我們會執行GPU的繪制指令來進行渲染。如果你在后續有執行類似移動這個view的位置等操作而需要再次渲染這個View時,我們就僅僅需要額外操作一次渲染指令就夠了。然而如果修改了View中的某些可見組件,那么之前的DisplayList就無法繼續使用了,我們需要回頭重新創建一個DisplayList並且重新執行渲染指令並更新到屏幕上。
需要注意的是:任何時候View中的繪制內容發生變化時,都會重新執行創建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。這個流程的表現性能取決於View的復雜程度,View的狀態變化以及渲染管道的執行性能。
所以我們需要盡量減少Overdraw。
Overdraw(過度繪制):描述的是屏幕上的某個像素在同一幀的時間內被繪制了多次。在多層次的UI結構里面,如果不可見的UI也在做繪制的操作,就會導致某些像素區域被繪制了多次,浪費大量的CPU以及GPU資源。(可以通過開發者選項,打開Show GPU Overdraw的選項,觀察UI上的Overdraw情況)。

藍色、淡綠、淡紅,深紅代表了4種不同程度的Overdraw的情況,我們的目標就是盡量減少紅色Overdraw,看到更多的藍色區域。
二、Android布局優化常用方法
綜上,布局的優化其實說白了就是減少層級,越簡單越好,減少overdraw,就能更好的突出性能。
下面介紹幾種布局優化的方式:
1、首先是善用相對布局Relativelayout
在RelativeLayout和LinearLayout同時能夠滿足需求時,盡量使用RelativeLayout,這一點可以從我們MainActivity默認布局就可以看出,默認是RelativeLayout,因為可以通過扁平的RelativeLayout降低LinearLayout嵌套所產生布局樹的層級。
Android提供了幾種方便的布局管理器,大多數時候,你只需要這些布局的一部分基本特性去實現UI。 一般情況下用LinearLayout的時候總會比RelativeLayout多一個View的層級。而每次往應用里面增加一個View,或者增加一個布局管理器的時候,都會增加運行時對系統的消耗,因此這樣就會導致界面初始化、布局、繪制的過程變慢。還是舉個例子吧,先看一下布局圖。
首先用LinearLayout方式來實現:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:src="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="16dp"
android:text="這個是LinearLayout"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:text="這個是LinearLayout,這個是LinearLayout"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

接着是RelativeLayout方式:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="16dp"
android:layout_toRightOf="@+id/iv_image"
android:text="這個是LinearLayout"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_title"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_toRightOf="@+id/iv_image"
android:text="這個是LinearLayout,這個是LinearLayout"
android:textSize="12sp" />
</RelativeLayout>

很明顯Relativelayout的層級比linearlayout的層級少了一層,這個界面比較簡單,但是如果界面很復雜的情況下,那么怎么來優化或者看到一目了然的層級樹呢?別擔心,Android自帶了工具,下面就介紹下Hierarchy View的簡單使用吧。
為了考慮安全問題,真機上不好嘗試,雖然也有辦法可以解決,但是現在模擬器還是挺快的,也不用折騰那么多了,就可以直接用模擬器來實現好了,接着模擬器運行app,打開需要獲取view層級的那個界面。
然后依次點擊菜單Tools -> Android -> Android Device Monitor。

打開Android Device Monitor后,選中Hierarchy View,然后通過Hierarchy View來獲取當前的View的分級圖。
接着我們就可以來看一下兩個分級的不同了。這里主要截取兩個不同的地方:
首先是LinearLayout:

接着是RelativeLayout:

很明顯的可以看出來RelativeLayout比LinearLayout少了一個層級,當然渲染的時間也是大大減少了。
2、布局優化的另外一種手段就是使用抽象布局標簽include、merge、ViewStub
2.1、首先是include標簽
include標簽常用於將布局中的公共部分提取出來,比如我們要在activity_main.xml中需要上述LinearLayout的數據,那么就可以直接include進去了。
<?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:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.jared.layoutoptimise.MainActivity">
<include layout="@layout/item_test_linear_layout" />
</RelativeLayout>
2.2、merge標簽:
merge標簽是作為include標簽的一種輔助擴展來使用,它的主要作用是為了防止在引用布局文件時產生多余的布局嵌套。
Android渲染需要消耗時間,布局越復雜,性能就越差。如上述include標簽引入了之前的LinearLayout之后導致了界面多了一個層級。

這個時候用merge的話,就可以減少一個層級了,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="16dp"
android:layout_toRightOf="@+id/iv_image"
android:text="這個是MergeLayout"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_title"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_toRightOf="@+id/iv_image"
android:text="這個是MergeLayout,這個是MergeLayout"
android:textSize="12sp" />
</merge>
activity_main就可以直接include了。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.jared.layoutoptimise.MainActivity">
<include layout="@layout/item_merge_layout" />
</RelativeLayout>
然后看下層級:
2.3、viewstub標簽:
viewstub是view的子類。他是一個輕量級View, 隱藏的,沒有尺寸的View。他可以用來在程序運行時簡單的填充布局文件。接着簡單試用下viewstub吧。首先修改activity_main.xml文件如下:
<?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:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.jared.layoutoptimise.MainActivity">
<include
android:id="@+id/layout_merge"
layout="@layout/item_merge_layout" />
<Button
android:id="@+id/btn_view_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="顯示ViewStub"
android:textAllCaps="false"
android:layout_below="@+id/tv_content"/>
<Button
android:id="@+id/btn_view_hide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="50dp"
android:layout_toRightOf="@+id/btn_view_show"
android:text="隱藏ViewStub"
android:textAllCaps="false"
android:layout_below="@+id/tv_content"/>
<ViewStub
android:id="@+id/vs_test"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/item_test_linear_layout"
android:layout_below="@+id/btn_view_show"
android:layout_marginTop="10dp" />
</RelativeLayout>
這里的ViewStub控件的layout指定為item_test_linear_layout。當點擊button隱藏的時候不會顯示item_test_linear_layout,而點擊button顯示的時候就會用item_test_linear_layout替代ViewStub。
3、Android最新的布局方式ConstaintLayout
ConstraintLayout允許你在不適用任何嵌套的情況下創建大型而又復雜的布局。它與RelativeLayout非常相似,所有的view都依賴於兄弟控件和父控件的相對關系。但是,ConstraintLayout比RelativeLayout更加靈活,目前在AndroidStudio中使用也十分方便,就和以前的拖拉控件十分相似。那么怎么使用呢?
首先是安裝Constaintlayout了。Android SDK -> SDK Tools -> Support Repository中的ConstrainLayout for Android和Solver for ConstaintLayout。

然后build.gradle中添加:
compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'
然后同步下就可以正常使用ConstaintLayout了。
接着我們來實現上述的布局文件,首先把布局按照constraintLayout的方式來布。效果如下,其實很像ios的布局,具體怎么用的就不一一介紹了:

看下代碼是怎樣的:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/lay_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:src="@mipmap/ic_launcher"
android:layout_marginStart="16dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/iv_image"
android:text="這個是ConstraintLayout"
android:textSize="16sp"
app:layout_constraintLeft_toRightOf="@+id/iv_image"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_title"
android:layout_toRightOf="@+id/iv_image"
android:text="這個是ConstraintLayout,這個是RelativeLayout"
android:textSize="12sp"
app:layout_constraintTop_toBottomOf="@+id/tv_title"
android:layout_marginTop="16dp"
app:layout_constraintLeft_toLeftOf="@+id/tv_title" />
</android.support.constraint.ConstraintLayout>
最后運行之后看下布局的層級如下:

這個是簡單的布局,如果是復雜的布局的話,那用Constaintlayout的話最多就兩個層級了,不像Relative和Linear一樣一層嵌套一層的。
4、利用Android Lint工具尋求可能優化布局的層次
一些Lint規則如下:
1、使用組合控件: 包含了一個ImageView以及一個TextView控件的LinearLayout如果能夠作為一個組合控件將會被更有效的處理。
2、合並作為根節點的幀布局(Framelayout) :如果一個幀布局時布局文件中的根節點,而且它沒有背景圖片或者padding等,更有效的方式是使用merge標簽替換該Framelayout標簽 。
3、無用的葉子節點:通常來說如果一個布局控件沒有子視圖或者背景圖片,那么該布局控件時可以被移除(由於它處於 invisible狀態)。
4、無用的父節點 :如果一個父視圖即有子視圖,但沒有兄弟視圖節點,該視圖不是ScrollView控件或者根節點,並且它沒有背景圖片,也是可以被移除的,移除之后,該父視圖的所有子視圖都直接遷移至之前父視圖的布局層次。同樣能夠使解析布局以及布局層次更有效。
5、過深的布局層次:內嵌過多的布局總是低效率地。考慮使用一些扁平的布局控件,例如 RelativeLayout、GridLayout ,來改善布局過程。默認最大的布局深度為10 。
By Hoolay-Android : 東月之神
參考:
http://www.oschina.net/news/60157/android-performance-patterns
https://developer.android.com/training/improving-layouts/index.html
