本文首發於微信公眾號「玉剛說」
1. 概述
現在的APP一些視覺效果都很炫,往往在一個介面上堆疊了很多檢視,這很容易出現一些效能的問題,嚴重的話甚至會造成卡頓。因此,我們在開發時必須要平衡好設計效果和效能的問題。
本文主要講解如何對檢視和佈局進行優化:包括如何避免過度繪製,如何減少佈局的層級,如何使用ConstraintLayout等等。
2. 過度繪製(Overdraw)
2.1 什麼是過度繪製?
過度繪製(Overdraw)指的是螢幕上的某個畫素在同一幀的時間內被繪製了多次。
舉個例子:在多層次的UI結構裡面,如果不可見的UI也進行繪製操作,那麼就會造成某些畫素區域被繪製了多次。這會浪費大量的CPU以及GPU資源。這是我們需要避免的。
2.2 如何檢測過度繪製
Android手機上面的開發者選項提供了工具來檢測過度繪製,可以按如下步驟來開啟:
開發者選項->除錯GPU過度繪製->顯示過度繪製區域
如下圖所示:
可以看到,介面上出現了一堆紅綠藍的區域,我們來看下這些區域代表什麼意思:
需要注意的是,有些過度繪製是無法避免的。因此在優化介面時,應該儘量讓大部分的介面顯示為真彩色(即無過度繪製)或者為藍色(僅有 1 次過度繪製)。儘量避免出現粉色或者紅色。
2.3 過度繪製優化
可以採取以下方案來減少過度繪製:
1.移除佈局中不需要的背景
2.將layout層級扁平化
3.減少透明度的使用
2.3.1 移除佈局中不需要的背景
一些佈局中的背景由於被該檢視上所繪製的內容完全覆蓋掉,因此這個背景實際上多餘的,如果沒有移除這個背景的話,將會產生過度繪製。我們可以使用以下方案來移除佈局中不需要的背景:
1.移除Window預設的Background
2.移除控制元件中不需要的背景
2.3.1.1 移除Window預設的Background
通常,我們使用的theme
都會包含了一個windowBackground
,比如某個theme
的如下:
<item name="android:windowBackground">@color/background_material_light</item>
複製程式碼
然後,我們一般會給佈局一個背景,比如:
<android.support.constraint.ConstraintLayout
...
android:background="@mipmap/bg">
複製程式碼
這就導致了整個頁面都會多了一次繪製。
那麼其解決辦法也很簡單,把windowBackground
移除掉就可以了,有以下兩種方法來解決,隨便使用其中一種即可:
1.在theme
中設定
<style name="AppTheme" parent="主題">
<item name="android:windowBackground">@null</item>
</style>
複製程式碼
2.在Activity
的onCreate()
方法中新增:
getWindow().setBackgroundDrawable(null);
複製程式碼
直接來看下優化前後的對比圖:
優化前,由於需要繪製windowBackground以及佈局的background,即有1次過度繪製,因此整個介面是藍色的,同時hello world文字部分再進行了一次繪製,所以變綠了。
優化後,由於不需要繪製windowBackground,僅僅只需要繪製佈局的background,因此整個介面顯示的是原本的真彩色。文字部分再進行一次繪製,也只是藍色而已。
2.3.1.2 移除控制元件中不需要的背景
下面先來看個例子,根佈局LinearLayout
設定了一個背景,然後它的子控制元件3個TextView
中有兩個設定了同樣的背景,佈局如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffffff"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffffffff"
android:text="test0"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffffffff"
android:text="test2"/>
</LinearLayout>
複製程式碼
其顯示結果如下:
可以看到,2個使用了跟父佈局同樣背景的TextView會導致了一次過度繪製。
那麼,我們平時只需要遵循以下兩個原則就可以減少次過度繪製:
1.對於子控制元件,如果其背景顏色跟父佈局一致,那麼就不用再給子控制元件新增背景了。
2.如果子控制元件背景五顏六色,且能夠完全覆蓋父佈局,那麼父佈局就可以不用新增背景了。
2.3.2 將layout層級扁平化
往往我們在寫介面的時候都會使用基本佈局來實現,這可能會出現一些效能問題。比如:使用巢狀的LinearLayout
可能會導致佈局的層次結構變得過深。另外,如果在LinearLayout
中使用了layout_weight
的話,那麼他的每一個子 view
都需要測量兩次。特別是用在 ListView
和 GridView
時,他們會被反覆測量。
佈局巢狀過多的話會導致過度繪製,從而降低效能,因此我們需要將佈局的層次結構儘量扁平化。
2.3.2.1 使用Layout Inspector去檢視layout的層次結構
之前的Android SDK工具包含了一個名為Hierarchy Viewer
的工具,可以在應用執行時分析佈局。但是在Android Studio 3.1之後,Hierarchy Viewer
就給移除掉了。並且Android的團隊表示不再開發Hierarchy Viewer
。所以這裡就不介紹Hierarchy Viewer
。
這裡使用Android推薦的Layout Inspector
來檢視layout的層次結構。
在Android Studio中點選Tools > Android > Layout Inspector
。然後在出現的 Choose Process
對話方塊中,選擇想要檢查的應用程式即可。
Layout Inspector
會自動捕獲快照,然後會顯示以下內容:
- View Tree:檢視在佈局中的層次結構。
- Screenshot:每個檢視可視邊界的裝置螢幕截圖。
- Properties Table:選定檢視的佈局屬性。
通過左側View Tree
即可看到佈局中的層次結構。
偷偷提一句,
Layout Inspector
也可以用來分析別人APP的佈局。
2.3.2.2 使用巢狀少的佈局
假如要實現以下佈局:
我們可以使用
LinearLayout
和RelativeLayout
來完成。但是LinearLayout
相比於RelativeLayout
,就多了一層,所以RelativeLayout
明顯是一個更優的選擇。如下圖所示:所以,合理選擇不同的佈局能夠減少巢狀。
2.3.2.3 使用merge標籤減少巢狀
通過<include>
標籤能夠複用佈局。
比如,我們要複用如下的一個佈局,一個垂直的線性佈局包含一個ImageView
和TextView
,其佈局檔案layout_include.xml
如下:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
...
/>
<TextView
...
/>
</LinearLayout>
複製程式碼
然後我們就可以通過<include>
來複用這個佈局了,其佈局檔案activity_include.xml
如下:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:orientation="vertical">
<include layout="@layout/layout_include"/>
<include layout="@layout/layout_include"/>
<include layout="@layout/layout_include"/>
</LinearLayout>
複製程式碼
但是上面這個例子會有個問題:其父佈局是垂直的線性佈局,include
進來的也是垂直的線性佈局,這就會造成了佈局巢狀,而且這種巢狀是沒必要的,那麼就可以使用<merge>
標籤來減少這種巢狀。將layout_include.xml
改成以下即可:
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
...
/>
<TextView
...
/>
</merge>
複製程式碼
我們可以用Layout Inspector
來看下使用<merge>
標籤優化前後的佈局層次結構:
2.3.2.4 使用lint來優化佈局的層次結構
lint
是一個靜態程式碼分析工具,可以用來協助優化佈局的效能。要使用lint
,點選Analyze
> Inspect Code
即可,如下圖所示:
佈局效能方面的資訊位於
Android
> Lint
> Performance
下,我們可以點開它來看下一些優化建議。下面是lint的一些優化技巧:
使用複合圖片
如果一個線性佈局中包含一個ImageView
和一個TextView
,可以使用複合圖片來替換掉合併根節點
如果一個FrameLayout
是整個佈局的根節點,並且也沒有提供背景、留白等等,那麼可以使用<merge>
標籤來替換掉,因為DecorView
本身就是一個FrameLayout
。移除佈局中無用的葉子
佈局是一個樹形的結構,如果一個佈局沒有子View
或者背景,那麼可以把它移除掉(這佈局本身就不可見了)。移除無用的父佈局
如果一個佈局沒有兄弟,也不是ScrollView
或者根View
,並且也沒有背景,那麼可以把這個父佈局移除掉,然後把它的子view
移到它的父佈局下。避免過深的層次結構
過多的佈局巢狀不利於效能,可以使用更扁平化的佈局,如RelativeLayout
、GridLayout
、ConstraintLayout
等佈局來提高效能。佈局預設的最大深度為10。
lint
的功能其實很強大,可以用來檢測優化各個方面,平時我們遇到lint的一些警告,能修復優化的話就儘量去完善掉。
2.3.3 減少透明度的使用
對於不透明的view
,只需要渲染一次即可把它顯示出來。但是如果這個view
設定了alpha
值,則至少需要渲染兩次。這是因為使用了alpha
的view
需要先知道混合view
的下一層元素是什麼,然後再結合上層的view
進行Blend混色處理。透明動畫、淡入淡出和陰影等效果都涉及到某種透明度,這就會造成了過度繪製。可以通過減少渲染這些透明物件來改善過度繪製。比如:在TextView
上設定帶透明度alpha
值的黑色文字可以實現灰色的效果。但是,直接通過設定灰色的話能夠獲得更好的效能。
2.3.4 減少自定義View的過度繪製,使用clipRect()
下面我們自定義一個View用來顯示多張重疊的表情包,效果圖如下:
其
onDraw()
方法也很簡單,就是遍歷所有表情包,然後繪製出來:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < imgs.length; i++) {
canvas.drawBitmap(imgs[i], i * 100, 0, mPaint);
}
}
複製程式碼
顯示過度繪製區域:
五顏六色的,過度繪製比較嚴重,那麼如何解決?
我們先來分析一下為什麼會出現過度繪製:以第一張圖為例,上面的程式碼會把整張圖都繪製出來了,第二張在第一張上面繼續繪製,這就造成了過度繪製。
那麼,解決辦法也很簡單,對於前面的n-1張圖,我們只需要繪製一部分即可,對於最後一張才繪製完整的。
Canvas
中的clipRect()
方法能夠設定一個裁剪矩形,只在這個矩形區域內的內容才能夠繪製出來。
優化後的程式碼如下:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < imgs.length; i++) {
canvas.save();
if (i < imgs.length - 1) {
//前面的n-1張圖,只裁剪一部分
canvas.clipRect(i * 100, 0, (i + 1) * 100, imgs[i].getHeight());
} else if (i == imgs.length - 1) {
//最後一張,完整的
canvas.clipRect(i * 100, 0, i * 100 + imgs[i].getWidth(), imgs[i].getHeight());
}
canvas.drawBitmap(imgs[i], i * 100, 0, mPaint);
canvas.restore();
}
複製程式碼
優化後的效果圖如下:
所有區域都是藍色的,即只有1次過度繪製。
Canvas
除了clipRect()
方法外,還有clipPath()
等方法,優化時選擇合理的方法去裁剪即可。
3.一些佈局優化技巧
除了避免過度繪製之外,還有一些其他的優化技巧能夠幫我們提升效能。這裡簡單介紹一下一些比較常用的技巧。
3.1 使用效能更優的佈局
- 在無巢狀佈局的情況下,
FrameLayout
和LinearLayout
的效能比RelativeLayout
更好。因為RelativeLayout
會測量每個子節點兩次。 ConstraintLayout
的效能比RelativeLayout
更好,推薦使用ConstraintLayout
。後面會介紹ConstraintLayout
的使用。
3.2 使用include標籤提高佈局的複用性
使用<include>
標籤提取佈局的公用部分,能夠提高佈局的複用性。具體例子這裡就不寫了,可以回頭看看<merge>
標籤那一小節的例子。
3.3 使用ViewStub標籤延遲載入
在專案中,有些複雜的佈局很少使用到,比如進度指示器等等。那麼我們可以通過<ViewStub>
標籤來實現在需要時才載入佈局。使用<ViewStub>
能夠減少記憶體的使用並且加快渲染速度。
ViewStub
是一個輕量級的檢視,它沒有尺寸,也不會繪製任何內容和參與佈局。下面是一個ViewStub
的例子:
<ViewStub
android:id="@+id/stub_import"
android:inflatedId="@+id/panel_import"
android:layout="@layout/progress_overlay"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
複製程式碼
這裡的panel_import
就是具體要載入的佈局ID。
通過以下程式碼即可在需要時載入佈局:
findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
或者
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
複製程式碼
一旦佈局載入後,ViewStub
就不再是原來佈局的一部分了,它會被新載入進來的佈局替換掉。需要注意的是,ViewStub
不支援<merge>
標籤。
3.4 onDraw()中不要建立新的區域性變數以及不要做耗時操作
onDraw()
中不要建立新的區域性變數,因為onDraw()
方法可能會被頻繁呼叫,大量的臨時物件會導致記憶體抖動,會造成頻繁的GC,從而使UI執行緒被頻繁阻塞,導致畫面卡頓。- Android要求每幀的繪製時間不超過16ms,在
onDraw()
進化耗時操作的話,輕則掉幀,嚴重的話會造成卡頓。
3.5 使用GPU呈現模式分析工具來分析渲染速度
點選開發者模式->監控->GPU呈現模式,然後選擇 在螢幕上顯示為條形圖 即可以看到一個圖表。
如下圖所示:
上圖中,主要包含了以下資訊:
1.沿水平軸的每個豎條都代表一個幀,每個豎條的高度表示渲染該幀所花的時間(單位:毫秒)。
2.水平綠線表示 16 毫秒。 要實現每秒 60 幀,代表每個幀的豎條需要保持在此線以下。 當豎條超出此線時,可能會使動畫出現暫停。
再來看下每個豎條的顏色代表什麼意思:
注意:這是在Android6.0以上才有的顏色,6.0以下只有3、4種,所以建議使用6.0以上的裝置來檢視。
如果存在一大段的豎條都超過了綠線,則我們可以去分析是哪個階段的時間花費比較多,然後針對性的去優化。
4. 使用ConstraintLayout
ConstraintLayout是Android新推出的一個佈局,其效能更好,連官方的hello world都用ConstraintLayout來寫了。所以極力推薦使用ConstraintLayout來編寫佈局。
ConstraintLayout,可以翻譯為約束佈局,在2016年Google I/O 大會上釋出。我們知道,當佈局巢狀過多時會出現一些效能問題。之前我們可以去通過RelativeLayout或者GridLayout來減少這種佈局巢狀的問題。現在,我們可以改用ConstraintLayout來減少佈局的層級結構。ConstraintLayout相比RelativeLayout,其效能更好,也更容易使用,結合Android Studio的佈局編輯器可以實現拖拽控制元件來編寫佈局等等。