如今的APP一些視覺效果都很炫,每每在一個界面上堆疊了不少視圖,這很容易出現一些性能的問題,嚴重的話甚至會形成卡頓。所以,咱們在開發時必需要平衡好設計效果和性能的問題。android
本文主要講解如何對視圖和佈局進行優化:包括如何避免過分繪製,如何減小布局的層級,如何使用ConstraintLayout等等。canvas
過分繪製(Overdraw)指的是屏幕上的某個像素在同一幀的時間內被繪製了屢次。編輯器
舉個例子:在多層次的UI結構裏面,若是不可見的UI也進行繪製操做,那麼就會形成某些像素區域被繪製了屢次。這會浪費大量的CPU以及GPU資源。這是咱們須要避免的。ide
Android手機上面的開發者選項提供了工具來檢測過分繪製,能夠按以下步驟來打開:工具
開發者選項->調試GPU過分繪製->顯示過分繪製區域佈局
以下圖所示:性能
顯示過分繪製區域.png優化
能夠看到,界面上出現了一堆紅綠藍的區域,咱們來看下這些區域表明什麼意思:動畫
overdraw.png線程
須要注意的是,有些過分繪製是沒法避免的。所以在優化界面時,應該儘可能讓大部分的界面顯示爲真彩色(即無過分繪製)或者爲藍色(僅有 1 次過分繪製)。儘可能避免出現粉色或者紅色。
能夠採起如下方案來減小過分繪製:
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); 複製代碼
直接來看下優化先後的對比圖:
移除Window默認的Background.png
優化前,因爲須要繪製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> 複製代碼
其顯示結果以下:
移除控件中不須要的背景.png
能夠看到,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
會自動捕獲快照,而後會顯示如下內容:
layout-inspector.png
經過左側View Tree
便可看到佈局中的層次結構。
偷偷提一句,
Layout Inspector
也能夠用來分析別人APP的佈局。
2.3.2.2 使用嵌套少的佈局
假如要實現如下佈局:
layout-listitem.png
咱們可使用LinearLayout
和RelativeLayout
來完成。可是LinearLayout
相比於RelativeLayout
,就多了一層,因此RelativeLayout
明顯是一個更優的選擇。以下圖所示:
過分嵌套LinearLayout.png
因此,合理選擇不一樣的佈局可以減小嵌套。
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>
標籤優化先後的佈局層次結構:
使用merge標籤減小嵌套.png
2.3.2.4 使用lint來優化佈局的層次結構
lint
是一個靜態代碼分析工具,能夠用來協助優化佈局的性能。要使用lint
,點擊Analyze
> Inspect Code
便可,以下圖所示:
lint-inspect-code.png
佈局性能方面的信息位於Android
> Lint
> Performance
下,咱們能夠點開它來看下一些優化建議。
lint-display.png
下面是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用來顯示多張重疊的表情包,效果圖以下:
自定義view_1.png
其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); } } 複製代碼
顯示過分繪製區域:
自定義view_2.png
五光十色的,過分繪製比較嚴重,那麼如何解決?
咱們先來分析一下爲何會出現過分繪製:以第一張圖爲例,上面的代碼會把整張圖都繪製出來了,第二張在第一張上面繼續繪製,這就形成了過分繪製。
那麼,解決辦法也很簡單,對於前面的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(); } 複製代碼
優化後的效果圖以下:
自定義view_3.png
全部區域都是藍色的,即只有1次過分繪製。
Canvas
除了clipRect()
方法外,還有clipPath()
等方法,優化時選擇合理的方法去裁剪便可。
除了避免過分繪製以外,還有一些其餘的優化技巧可以幫咱們提高性能。這裏簡單介紹一下一些比較經常使用的技巧。
FrameLayout
和LinearLayout
的性能比RelativeLayout
更好。由於RelativeLayout
會測量每一個子節點兩次。ConstraintLayout
的性能比RelativeLayout
更好,推薦使用ConstraintLayout
。後面會介紹ConstraintLayout
的使用。使用<include>
標籤提取佈局的公用部分,可以提升佈局的複用性。具體例子這裏就不寫了,能夠回頭看看<merge>
標籤那一小節的例子。
在項目中,有些複雜的佈局不多使用到,好比進度指示器等等。那麼咱們能夠經過<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>
標籤。
onDraw()
中不要建立新的局部變量,由於onDraw()
方法可能會被頻繁調用,大量的臨時對象會致使內存抖動,會形成頻繁的GC,從而使UI線程被頻繁阻塞,致使畫面卡頓。onDraw()
進化耗時操做的話,輕則掉幀,嚴重的話會形成卡頓。點擊開發者模式->監控->GPU呈現模式,而後選擇 在屏幕上顯示爲條形圖 便可以看到一個圖表。
以下圖所示:
GPU呈現模式分析.png
上圖中,主要包含了如下信息:
1.沿水平軸的每一個豎條都表明一個幀,每一個豎條的高度表示渲染該幀所花的時間(單位:毫秒)。
2.水平綠線表示 16 毫秒。 要實現每秒 60 幀,表明每一個幀的豎條須要保持在此線如下。 當豎條超出此線時,可能會使動畫出現暫停。
再來看下每一個豎條的顏色表明什麼意思:
注意:這是在Android6.0以上纔有的顏色,6.0如下只有三、4種,因此建議使用6.0以上的設備來查看。
用GPU呈現模式分析-區段說明.png
若是存在一大段的豎條都超過了綠線,則咱們能夠去分析是哪一個階段的時間花費比較多,而後針對性的去優化。
ConstraintLayout是Android新推出的一個佈局,其性能更好,連官方的hello world都用ConstraintLayout來寫了。因此極力推薦使用ConstraintLayout來編寫佈局。
ConstraintLayout,能夠翻譯爲約束佈局,在2016年Google I/O 大會上發佈。咱們知道,當佈局嵌套過多時會出現一些性能問題。以前咱們能夠去經過RelativeLayout或者GridLayout來減小這種佈局嵌套的問題。如今,咱們能夠改用ConstraintLayout來減小布局的層級結構。ConstraintLayout相比RelativeLayout,其性能更好,也更容易使用,結合Android Studio的佈局編輯器能夠實現拖拽控件來編寫佈局等等。