那些 Android 程式設計師必會的檢視優化策略

玉剛說發表於2018-09-20

本文首發於微信公眾號「玉剛說」

原文連結:那些 Android 程式設計師必會的檢視優化策略

1. 概述

現在的APP一些視覺效果都很炫,往往在一個介面上堆疊了很多檢視,這很容易出現一些效能的問題,嚴重的話甚至會造成卡頓。因此,我們在開發時必須要平衡好設計效果和效能的問題。

本文主要講解如何對檢視和佈局進行優化:包括如何避免過度繪製,如何減少佈局的層級,如何使用ConstraintLayout等等。

2. 過度繪製(Overdraw)

2.1 什麼是過度繪製?

過度繪製(Overdraw)指的是螢幕上的某個畫素在同一幀的時間內被繪製了多次。

舉個例子:在多層次的UI結構裡面,如果不可見的UI也進行繪製操作,那麼就會造成某些畫素區域被繪製了多次。這會浪費大量的CPU以及GPU資源。這是我們需要避免的。

2.2 如何檢測過度繪製

Android手機上面的開發者選項提供了工具來檢測過度繪製,可以按如下步驟來開啟:

開發者選項->除錯GPU過度繪製->顯示過度繪製區域

如下圖所示:

顯示過度繪製區域.png
顯示過度繪製區域.png

可以看到,介面上出現了一堆紅綠藍的區域,我們來看下這些區域代表什麼意思:

overdraw.png
overdraw.png

需要注意的是,有些過度繪製是無法避免的。因此在優化介面時,應該儘量讓大部分的介面顯示為真彩色(即無過度繪製)或者為藍色(僅有 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.在ActivityonCreate()方法中新增:

    getWindow().setBackgroundDrawable(null);
複製程式碼

直接來看下優化前後的對比圖:

移除Window預設的Background.png
移除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
移除控制元件中不需要的背景.png

可以看到,2個使用了跟父佈局同樣背景的TextView會導致了一次過度繪製。

那麼,我們平時只需要遵循以下兩個原則就可以減少次過度繪製:

1.對於子控制元件,如果其背景顏色跟父佈局一致,那麼就不用再給子控制元件新增背景了。
2.如果子控制元件背景五顏六色,且能夠完全覆蓋父佈局,那麼父佈局就可以不用新增背景了。

2.3.2 將layout層級扁平化

往往我們在寫介面的時候都會使用基本佈局來實現,這可能會出現一些效能問題。比如:使用巢狀的LinearLayout可能會導致佈局的層次結構變得過深。另外,如果在LinearLayout中使用了layout_weight的話,那麼他的每一個子 view都需要測量兩次。特別是用在 ListViewGridView 時,他們會被反覆測量。

佈局巢狀過多的話會導致過度繪製,從而降低效能,因此我們需要將佈局的層次結構儘量扁平化。

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:選定檢視的佈局屬性。
layout-inspector.png
layout-inspector.png

通過左側View Tree即可看到佈局中的層次結構。

偷偷提一句,Layout Inspector也可以用來分析別人APP的佈局。

2.3.2.2 使用巢狀少的佈局

假如要實現以下佈局:

layout-listitem.png
layout-listitem.png

我們可以使用LinearLayoutRelativeLayout來完成。但是LinearLayout相比於RelativeLayout,就多了一層,所以RelativeLayout明顯是一個更優的選擇。如下圖所示:
過度巢狀LinearLayout.png
過度巢狀LinearLayout.png

所以,合理選擇不同的佈局能夠減少巢狀。

2.3.2.3 使用merge標籤減少巢狀

通過<include>標籤能夠複用佈局。

比如,我們要複用如下的一個佈局,一個垂直的線性佈局包含一個ImageViewTextView,其佈局檔案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
使用merge標籤減少巢狀.png
2.3.2.4 使用lint來優化佈局的層次結構

lint是一個靜態程式碼分析工具,可以用來協助優化佈局的效能。要使用lint,點選Analyze> Inspect Code即可,如下圖所示:

lint-inspect-code.png
lint-inspect-code.png

佈局效能方面的資訊位於Android> Lint> Performance下,我們可以點開它來看下一些優化建議。
lint-display.png
lint-display.png

下面是lint的一些優化技巧:

  1. 使用複合圖片
    如果一個線性佈局中包含一個 ImageView 和一個 TextView,可以使用複合圖片來替換掉

  2. 合併根節點
    如果一個FrameLayout 是整個佈局的根節點,並且也沒有提供背景、留白等等,那麼可以使用<merge>標籤來替換掉,因為DecorView本身就是一個FrameLayout

  3. 移除佈局中無用的葉子
    佈局是一個樹形的結構,如果一個佈局沒有子 View 或者背景,那麼可以把它移除掉(這佈局本身就不可見了)。

  4. 移除無用的父佈局
    如果一個佈局沒有兄弟,也不是ScrollView 或者根 View,並且也沒有背景,那麼可以把這個父佈局移除掉,然後把它的子view移到它的父佈局下。

  5. 避免過深的層次結構
    過多的佈局巢狀不利於效能,可以使用更扁平化的佈局,如RelativeLayoutGridLayoutConstraintLayout等佈局來提高效能。佈局預設的最大深度為10。

lint的功能其實很強大,可以用來檢測優化各個方面,平時我們遇到lint的一些警告,能修復優化的話就儘量去完善掉。

2.3.3 減少透明度的使用

對於不透明的view,只需要渲染一次即可把它顯示出來。但是如果這個view設定了alpha值,則至少需要渲染兩次。這是因為使用了alphaview需要先知道混合view的下一層元素是什麼,然後再結合上層的view進行Blend混色處理。透明動畫、淡入淡出和陰影等效果都涉及到某種透明度,這就會造成了過度繪製。可以通過減少渲染這些透明物件來改善過度繪製。比如:在TextView上設定帶透明度alpha值的黑色文字可以實現灰色的效果。但是,直接通過設定灰色的話能夠獲得更好的效能。

2.3.4 減少自定義View的過度繪製,使用clipRect()

下面我們自定義一個View用來顯示多張重疊的表情包,效果圖如下:

自定義view_1.png
自定義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 * 1000, mPaint);
        }
    }
複製程式碼

顯示過度繪製區域:

自定義view_2.png
自定義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 * 1000, (i + 1) * 100, imgs[i].getHeight());
            } else if (i == imgs.length - 1) {
                //最後一張,完整的
                canvas.clipRect(i * 1000, i * 100 + imgs[i].getWidth(), imgs[i].getHeight());
            }
            canvas.drawBitmap(imgs[i], i * 1000, mPaint);
            canvas.restore();
        }
複製程式碼

優化後的效果圖如下:

自定義view_3.png
自定義view_3.png

所有區域都是藍色的,即只有1次過度繪製。

Canvas除了clipRect()方法外,還有clipPath()等方法,優化時選擇合理的方法去裁剪即可。

3.一些佈局優化技巧

除了避免過度繪製之外,還有一些其他的優化技巧能夠幫我們提升效能。這裡簡單介紹一下一些比較常用的技巧。

3.1 使用效能更優的佈局

  1. 在無巢狀佈局的情況下,FrameLayoutLinearLayout的效能比RelativeLayout更好。因為RelativeLayout會測量每個子節點兩次。
  2. 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()中不要建立新的區域性變數以及不要做耗時操作

  1. onDraw()中不要建立新的區域性變數,因為onDraw()方法可能會被頻繁呼叫,大量的臨時物件會導致記憶體抖動,會造成頻繁的GC,從而使UI執行緒被頻繁阻塞,導致畫面卡頓。
  2. Android要求每幀的繪製時間不超過16ms,在onDraw()進化耗時操作的話,輕則掉幀,嚴重的話會造成卡頓。

3.5 使用GPU呈現模式分析工具來分析渲染速度

點選開發者模式->監控->GPU呈現模式,然後選擇 在螢幕上顯示為條形圖 即可以看到一個圖表。

如下圖所示:

GPU呈現模式分析.png
GPU呈現模式分析.png

上圖中,主要包含了以下資訊:

1.沿水平軸的每個豎條都代表一個幀,每個豎條的高度表示渲染該幀所花的時間(單位:毫秒)。
2.水平綠線表示 16 毫秒。 要實現每秒 60 幀,代表每個幀的豎條需要保持在此線以下。 當豎條超出此線時,可能會使動畫出現暫停。

再來看下每個豎條的顏色代表什麼意思:
注意:這是在Android6.0以上才有的顏色,6.0以下只有3、4種,所以建議使用6.0以上的裝置來檢視。

用GPU呈現模式分析-區段說明.png
用GPU呈現模式分析-區段說明.png

如果存在一大段的豎條都超過了綠線,則我們可以去分析是哪個階段的時間花費比較多,然後針對性的去優化。

4. 使用ConstraintLayout

ConstraintLayout是Android新推出的一個佈局,其效能更好,連官方的hello world都用ConstraintLayout來寫了。所以極力推薦使用ConstraintLayout來編寫佈局。

ConstraintLayout,可以翻譯為約束佈局,在2016年Google I/O 大會上釋出。我們知道,當佈局巢狀過多時會出現一些效能問題。之前我們可以去通過RelativeLayout或者GridLayout來減少這種佈局巢狀的問題。現在,我們可以改用ConstraintLayout來減少佈局的層級結構。ConstraintLayout相比RelativeLayout,其效能更好,也更容易使用,結合Android Studio的佈局編輯器可以實現拖拽控制元件來編寫佈局等等。

那些 Android 程式設計師必會的檢視優化策略
歡迎關注我的微信公眾號「玉剛說」,接收第一手技術乾貨

相關文章