Android UI性能優化

多數手機的屏幕刷新頻率是60hz,在1000/60=16.67ms內沒有辦法把這一幀的繪製任務執行完畢,就會發生丟幀的現象。丟幀越多,用戶感受到的卡頓情況就越嚴重。這裏的繪製包含了所有View的meature、layout、draw等,CPU的計算,以及GPU的柵格化渲染等一系列操作,也就是說,一般我們需要在16ms以內完成單次繪製的所有工作,才能保證app的流暢。
拋去CPU在meature、layout、draw過程中的各種不恰當的耗時操作(如處理大圖片等), Android UI問題一般就是過度繪製,即屏幕上某一像素點在一幀中被重複繪製多次。其解決方法簡單概括就是:

  • 通過Hierarchy Viewer去檢測渲染效率,去除不必要的嵌套
  • 通過Show GPU Overdraw去檢測Overdraw,最終可以通過移除不必要的背景,以及使用canvas.clipRect解決大多數覆蓋繪製問題。
    這裏寫圖片描述

過度繪製調試工具

GPU呈現模式分析

這裏寫圖片描述
此工具可以記錄每一幀繪製所使用的總時長,其中綠橫線代表16ms,每一幀豎線代表的含義:
這裏寫圖片描述
其他具體可見此處。若發現超過16ms綠線的幀較多或較密集,可以使用下面的工具進一步確定。

ADM中的Dump

這裏寫圖片描述
使用此工具,獲取佈局層次結構,根據具體情況,手動去除不必要的層級。之後,用以下工具做進一步處理。

GPU過度繪製調試工具

這裏寫圖片描述
以顏色塊表示出像素點被繪製過多少次的工具,過度繪製的主要調試工具。
假設頁面佈局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<!--1x OverDraw-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="#fff"
              android:gravity="bottom">
    <!--2x OverDraw-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:background="#fff"
        android:gravity="bottom">
        <!--3x OverDraw-->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="#fff"
            android:gravity="bottom">
            <!--4x+ OverDraw-->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#fff"
                android:gravity="bottom">
                <!--4x+ OverDraw-->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#fff"
                    android:gravity="bottom">
                    <!--4x+ OverDraw-->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="50dp"
                        android:background="#fff"
                        android:gravity="bottom">
                    </LinearLayout>
                </LinearLayout>
            </LinearLayout>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

開啓過度繪製之後可以看到:
這裏寫圖片描述
如果顏色爲原色,則表示像素只被繪製過一次,是最理想的情況。

解決過度繪製的幾個方法

引發過度繪製的原因,不外乎以下幾點:
1. 頁面UI嵌套層級深;
2. 同一區域裏,出現一個元素被另一個元素覆蓋的情況;
3. 背景顏色覆蓋(其實跟1是同樣的的問題)。

解決背景顏色覆蓋問題

一般情況下,如果不設置Activity中theme的android:windowBackground屬性的話,DecorView會默認的使用黑色背景:

/**
 * Returns the color used to fill areas the app has not rendered content to yet when the
 * user is resizing the window of an activity in multi-window mode.
 */
public static Drawable getResizingBackgroundDrawable(Context context, int backgroundRes,
        int backgroundFallbackRes, boolean windowTranslucent) {
    if (backgroundRes != 0) {
        final Drawable drawable = context.getDrawable(backgroundRes);
        if (drawable != null) {
            return enforceNonTranslucentBackground(drawable, windowTranslucent);
        }
    }

    if (backgroundFallbackRes != 0) {
        final Drawable fallbackDrawable = context.getDrawable(backgroundFallbackRes);
        if (fallbackDrawable != null) {
            return enforceNonTranslucentBackground(fallbackDrawable, windowTranslucent);
        }
    }
    return new ColorDrawable(Color.BLACK);
}

所以,不使用android:windowBackground的話,將其設置成透明色,可以全局的減少一層繪製,及其划算。上例中設置windowBackground爲透明後:
這裏寫圖片描述

同理,在佈局時,應去掉佈局文件中不必要的background設置,以此減少過度繪製的次數。

解決頁面UI嵌套層級深問題

使用Hierarchy Viewer可以直觀地看到佈局的層次結構,但我們大多數時候,都可以直接修改佈局文件來減少嵌套過深的問題。基本原則是是我們的佈局儘量“扁平化”,佈局中的嵌套層次以及冗餘層次儘量減少,如下例中:
佈局1:

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#e5e5e5"
        android:orientation="horizontal"
        android:padding="10dp">

        <ImageView
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:src="@mipmap/ic_launcher"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:orientation="vertical">

            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="20dp">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="姓名"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="right"
                    android:text="年齡"/>
            </FrameLayout>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="社會主義好青年"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="個人簡介:富強、民主、文明、和諧、自由、平等、公正、法治、愛國、敬業、誠信、友善"/>
        </LinearLayout>
    </LinearLayout>

佈局二:

<RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp"
        android:background="#e5e5e5"
        android:orientation="horizontal"
        android:padding="10dp">

        <ImageView
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:src="@mipmap/ic_launcher"/>

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="70dp"
            android:text="姓名"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="年齡"/>

        <TextView
            android:id="@+id/tv_tag"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@+id/tv_name"
            android:layout_below="@+id/tv_name"
            android:layout_marginTop="10dp"
            android:text="社會主義好青年"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@+id/tv_tag"
            android:layout_below="@+id/tv_tag"
            android:layout_marginTop="10dp"
            android:text="個人簡介:富強、民主、文明、和諧、自由、平等、公正、法治、愛國、敬業、誠信、友善"/>

    </RelativeLayout>

兩者的顯示效果是一樣的:
這裏寫圖片描述
但是佈局二中,少繪製了很多佈局一中的冗餘控件和層次,使用Hierachy Viewe的話,可以明顯的看出兩者的差距(Hierachy Viewer一般無法在真機上使用,此處就不做比較結果了…)。

其次,在需要有條件的動態顯示View的情況下,使用ViewStub先佔位,可以避免直接先將所有的View實例化(即使Gone的情況也會被實例化),而在ViewStub使用inflate方法後,具體的View纔會被實例化,這樣,就可以使用ViewStub,來方便的在運行時決定,要不要顯示某個佈局或者具體要顯示哪個佈局。

另外,使用merge作爲xml的根標籤,也可以在子視圖不需要指定任何針對父視圖的佈局屬性(layout_gravity等),或者父視圖與當前根節點是同一種ViewGroup時,幹掉一個view層級。

解決元素覆蓋問題

先來看一個例子,顯示一個疊加的橫向圖片列表:

public class Card extends View {
    private int picWidth;
    private int space;
    private Bitmap bm;
    private Paint paint;

    public Card(Context context) {
        super(context);
        init();
    }

    public Card(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public Card(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        picWidth = getResources().getDimensionPixelOffset(R.dimen.dp150);
        space = getResources().getDimensionPixelOffset(R.dimen.dp50);
        bm = BitmapFactory.decodeResource(getResources(), R.mipmap.ysl);
        paint = new Paint();
        paint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);
        int n = (getWidth()  - picWidth) / space;
        for (int i = 0; i < n; i++) {
            drawPic(canvas, space * i, picWidth);
        }
    }

    private void drawPic(Canvas canvas, int marginLeft, int size) {
        Rect recDes = new Rect(marginLeft, 0, marginLeft + size, size);
        canvas.drawBitmap(bm, new Rect(0, 0, getWidth(), getHeight()),
                recDes, paint);
    }
}

此時的表現爲:
這裏寫圖片描述
可以看到,中間紅、綠色部分本應是被蓋住的部分,依然多繪製了一次以上。我們的期望是,只繪製每張圖片沒有“相交”的部分,此時就用到了canvas.clipRece:

/**
* Intersect the current clip with the specified rectangle, which is
* expressed in local coordinates.
*
* @param left   The left side of the rectangle to intersect with the
*               current clip
* @param top    The top of the rectangle to intersect with the current clip
* @param right  The right side of the rectangle to intersect with the
*               current clip
* @param bottom The bottom of the rectangle to intersect with the current
*               clip
* @return       true if the resulting clip is non-empty
*/
public boolean clipRect(int left, int top, int right, int bottom) {
   return nClipRect(mNativeCanvasWrapper, left, top, right, bottom,
           Region.Op.INTERSECT.nativeInt);
}

簡單來說,這個clipRect就是對canvas畫布進行裁剪,即只在定義的矩形範圍內繪製,運用後修改原代碼爲:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int n = (getWidth() - picWidth) / space;
    for (int i = 0; i < n; i++) {
        int left = space * i;
        canvas.save();
        int right = left + (i < n - 1 ? space : picWidth);
        // 只裁剪出需要繪製的區域
        canvas.clipRect(left, 0, right, picWidth);
        drawPic(canvas, left, picWidth);
        canvas.restore();
    }
}

private void drawPic(Canvas canvas, int marginLeft, int size) {
    Rect recDes = new Rect(marginLeft, 0, marginLeft + size, size);
    canvas.drawBitmap(bm, new Rect(0, 0, getWidth(), getHeight()),
            recDes, paint);
}

現在效果如下:
這裏寫圖片描述
實際上,熟悉一下canvas的具體用法,以及clip的具體用法等,能發現更多值得探索和優化的東西,此處就不一一列舉了。

至於View之間的疊加引發的過度繪製(很多時候都是這種情況),目前鄙人還沒找到很好的解決方法,只能在開發中儘量避免了。

避免過度優化

減少過度繪製確實可以提升app性能,但一味的追求所謂的優化,有時候也是很浪費時間的。比如明明簡簡單單的就能用疊加控件的方式,實現一些交叉比較小的動態畫廊控件,爲了追求完美,花了大量時間去計算運動過程中的疊加部分,結果反而會讓cpu計算時間增長,與初衷相背馳了。因此,在實際設計和開發時,需要在複雜的邏輯,與簡單易用的界面中做一個平衡。

Hierarchy Viewer

上面的工具是可以比較直觀的去發現優化方案的,對於要精細把握app整體頁面的渲染情況是,可以使用Hierarchy Viewer。此工具提供了可視化的頁面佈局層次結構,並可得到每個view的具體meature、layout、draw的耗時,非常直觀的幫助我們優化佈局。
Hierarchy Viewer只能在開發版手機(幾乎沒有)以及模擬器上使用,使用ViewServer庫也可以在普通android機上使用,但是在部分國產機上無效,且在需要調試的Activity中需添加部分代碼,麻煩,建議還是以模擬器的方式來使用。
食用方式:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章