效能優化技巧知識梳理(1) 佈局優化

澤毛發表於2017-12-21

一、前言

效能優化包含的部分很多,包括佈局、記憶體、耗電、流量等等,其中佈局優化是最容易掌握,也最容易被大家所忽視的一個方面,今天,就來介紹一下有關佈局優化的一些技巧。

二、佈局優化技巧

(1) 使用 標籤進行佈局複用

當我們的佈局中有多個相同的佈局時,可以使用include標籤來進行佈局的複用,這樣,當視覺需要修改單個Item的間距,文字大小時,只需要修改一個佈局就可以了,例如像下面這種情況,我們就可以使用include標籤來實現:

效能優化技巧知識梳理(1)   佈局優化
根佈局為:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <include android:id="@+id/include_1" layout="@layout/layout_is_merge" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
    <include android:id="@+id/include_2" layout="@layout/layout_is_merge" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
    <include android:id="@+id/include_3" layout="@layout/layout_is_merge" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</LinearLayout>
複製程式碼

單個Item的佈局為:

<?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">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/tv_content_1"
            android:text="tv_content_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:id="@+id/tv_content_2"
            android:text="tv_content_2"
            android:layout_marginLeft="40dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</merge>
複製程式碼

要點

  • 直接在根佈局中,如果希望找到<include>所指定的layout中包含的控制元件,那麼就需要給<include>指定id,再通過它來尋找子容器中的控制元件。
  • <include>標籤中,可以指定layout_xxx屬性,它將會覆蓋子佈局中的根標籤中的屬性。

(2) 使用 標籤減少佈局層級

當出現下面這種情況:一個xml佈局檔案的根節點是一個FrameLayout,並且它沒有一個有用的背景,那麼當該xml佈局檔案渲染出的ViewGroup被新增到父佈局中時,連線處就會出現一個多餘的節點,而採用<merge>標籤可以去掉這一無用節點,從而降低佈局的層級。

例如,在上面的例子當中,我們使用了<merge>標籤的情形為:

效能優化技巧知識梳理(1)   佈局優化
假如我們沒有使用<merge>標籤,那麼情形為:
效能優化技巧知識梳理(1)   佈局優化
要點

  • 當需要通過LayoutInflaterinflate方法渲染出以<merge>作為根節點標籤的xml檔案時,必須傳入不為nullroot引數,且attachToRoot引數必須為true
  • <merge>只可作為xml的根節點。
  • <merge>既不是View也不是ViewGroup,它只是表示一組等待被新增的檢視,因此,對它設定的任何屬性都是無用的。

(3) 使用 ViewStub 標籤動態載入佈局

當我們的佈局中,存在一些需要按序載入的控制元件,那麼就可以使用ViewStub標籤預先宣告,當情況滿足時再去例項化ViewStub中所宣告的佈局,其用法如下:

  • 首先,在佈局中預先宣告ViewStub,並且通過layout標籤指定對應的佈局layout_stub
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ViewStub
        android:id="@+id/view_stub"
        android:inflatedId="@+id/view_inflated"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/layout_stub"/>
</LinearLayout>
複製程式碼
  • 當需要載入以上指定的佈局時,那麼首先通過獲得ViewStub,再呼叫它的inflate或者setVisibility(View.VISIBLE)方法,其返回的佈局就是layout=所指定的佈局的根節點:
    private void inflateIfNeed() {
        //1.獲取到佈局中的ViewStub。
        mViewStub = (ViewStub) findViewById(R.id.view_stub);
        //2.呼叫其inflate方法例項化它所指定的layout。
        mStubView = mViewStub.inflate();
    }
複製程式碼

要點

  • 任何ViewStub只能呼叫一次inflate或者setVisibility(View.VISIBLE)方法,並且呼叫完之後它將不再可用,ViewStub原先所在位置將被替換成為layout引數所指定的佈局的根節點,並且其根節點的id值將變成android:inflatedId所指定的值:
    效能優化技巧知識梳理(1)   佈局優化

(4) 選擇合適的父容器以減少佈局層級和測量次數

當我們需要通過父容器來容納多個子控制元件時,如何選擇父容器,將會影響到佈局的效率,而對於父容器的選擇,有以下幾點原則:

  • 首先應當考慮佈局層級最小的方案。
  • 佈局層級相同時,就應當選取合適的父容器,一般來說,有以下幾點經驗:
  • 選取的優先順序為:FrameLayout、不帶layout_weight引數的LinearLayoutRelativeLayout,這裡選取的標準為帶有layout_weightLinearLayout或者RelativeLayout會測量兩次。
  • 當使用LinearLayout時,應當儘量避免使用layout_weight引數。
  • 避免使用RelativeLayout巢狀RelativeLayout
  • 如果允許,那麼可以使用Google新推出的ConstraintLayout佈局。

(5) 使用 SpannableStringBuilder 替換多個 TextView 的實現

當我們存在多種不同大小、顏色或者圖文混排需要顯示時,我們往往會利用多個TextView來進行組合,但是某些效果通過一個TextView就可以實現,一般來說,利用SpannableStringBuilder可以通過單個TextView實現多種不同的佈局,更多Span的用法可以參考這篇文章:Android 中各種 Span 的用法,下面以不同大小的TextView為例:

    private void useSpan() {
        TextView textView = (TextView) findViewById(R.id.tv_span);
        SpannableStringBuilder ssb = new SpannableStringBuilder("300 RMB");
        //設定文字大小。
        ssb.setSpan(new RelativeSizeSpan(6.0f), 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        //設定文字顏色。
        ssb.setSpan(new ForegroundColorSpan(0xff303F9F), 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        textView.setText(ssb);
    }
複製程式碼

最終可以實現如下的效果:

效能優化技巧知識梳理(1)   佈局優化
除此之外,還可以實現圖文混排,例如下面這樣:
效能優化技巧知識梳理(1)   佈局優化

(6) 使用 LinearLayout 自帶的分割線,而不是在佈局中手動新增一個 ImageView

例如下面的佈局:

效能優化技巧知識梳理(1)   佈局優化
此時我們就可以使用LinearLayout自帶的divider屬性來實現分割線:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:showDividers="beginning|end|middle"
    android:divider="@android:drawable/divider_horizontal_bright"
    android:dividerPadding="5dp"
    android:paddingTop="20dp">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Line 1"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Line 2"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Line 3"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Line 4"/>
</LinearLayout>
複製程式碼

與分割線相關的屬性包括以下幾個:

  • divider:傳入分割線的drawable,可以是一個圖片,也可以是自己通過xml實現的drawable
  • showDividers:分割線顯示的位置,beginning/middle/end,分割對應頭部、中間、尾部。
  • dividerPadding:分割線距離兩邊的間距。

(7) 使用 Space 控制元件進行合理的佔位

Space控制元件位於android.support.v4.widget包中,與一般控制元件不同,它的draw方法是一個空實現,因此它只佔位置,而不去渲染,使用它來進行佔位填充比其它控制元件更加高效,例如下面,我們需要將一行均等地分成五份,有顏色部分位於2,4當中:

效能優化技巧知識梳理(1)   佈局優化
這時候,就可以通過Space控制元件,加上layout_weight屬性來實現:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v4.widget.Space
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"/>
    <View
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"
        android:background="@color/colorAccent"/>
    <android.support.v4.widget.Space
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"/>
    <View
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"
        android:background="@color/colorAccent"/>
    <android.support.v4.widget.Space
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"/>
</LinearLayout>
複製程式碼

(8) 使用 TextView 的 drawableLeft/drawableTop 屬性來替代 ImageView + TextView 的佈局

當出現圖片在文案的四周時,我們應當首先考慮能夠通過單個TextView來實現,而不是通過LinearLayout包裹TextView+ImageView的方式來實現,例如下面的效果:

效能優化技巧知識梳理(1)   佈局優化
其佈局如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- 方式一:使用 ImageView + TextView -->
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical">
        <ImageView
            android:src="@android:drawable/ic_btn_speak_now"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:text="ImageView + TextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <!-- 方式二:使用單個 TextView -->
    <TextView
        android:drawableLeft="@android:drawable/ic_btn_speak_now"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:text="單個 TextView"/>
</LinearLayout>
複製程式碼

可以看到,雖然都是實現了圖片加上文字的顯示效果,但是第二種通過單個TextView來實現其佈局層級更少,並且控制元件的個數更少,因此效率更高,並且圖片不僅可以顯示在左邊,還可以顯示在TextView的四周,圖片和TextView之間的間隔可以通過drawablePadding來實現。

(9) 去掉不必要的背景

  • 在佈局層級中避免重疊部分的背景

當兩個控制元件在佈局上有重疊的部分,但是它們具有背景時,就會出現過度繪製的情況,造成無用的效能損耗。並且肉眼無法發現,需要通過設定當中的”除錯GPU過度繪製"選項進行檢查,詳細使用如下:效能優化工具知識梳理(3) - 除錯GPU過度繪製 & GPU呈現模式分析。例如下面佈局當中,根佈局和子控制元件有100dp部分重疊,並且它們都有背景:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF">
    <LinearLayout
        android:background="#FFFFFF"
        android:layout_width="match_parent"
        android:layout_height="100dp"/>
</LinearLayout>
複製程式碼

那麼最終,開啟過度繪製檢測時,就會出現下面的效果:

效能優化技巧知識梳理(1)   佈局優化

  • 去掉無用的WindowBackgroud 當我們使用某些主題時,系統有可能在DecorView中給我們加上一個背景,但是有時候它是無用的,例如上面的例子中,我們根佈局為紫色,這其實就是由於預設主題中的背景所導致的,我們可以通過下面的方式去除掉該背景。
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_overdraw);
        getWindow().setBackgroundDrawable(null);
    }
複製程式碼

此時的檢測結果如下,可以看到,根佈局就不存在過度繪製的情況了:

效能優化技巧知識梳理(1)   佈局優化

(10) 優化自定義控制元件中的 onDraw 方法

當我們在自定義控制元件,並重寫onDraw方法來完成相應的需求時,一些錯誤的操作往往會導致佈局效率的降低,一般來說,有兩點需要注意:

  • 避免在其中進行物件的分配
  • 使用CanvasClipRect方法避免過度繪製

這裡用一個簡單的例子來說明一下第二點的實現,當我們需要實現下面這個多張圖片重疊的自定義控制元件時:

效能優化技巧知識梳理(1)   佈局優化
假如我們直接使用下面的方式,也可以實現上面的效果:

public class ClipRectView extends View {

    private static final int[] ID = new int[]{R.drawable.pic_1, R.drawable.pic_2, R.drawable.pic_3};
    private Bitmap[] mBitmaps;

    public ClipRectView(Context context) {
        super(context);
        prepareBitmap();
    }

    public ClipRectView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        prepareBitmap();
    }

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

    private void prepareBitmap() {
        mBitmaps = new Bitmap[ID.length];
        int i = 0;
        for (int id : ID) {
            mBitmaps[i++] = BitmapFactory.decodeResource(getResources(), id);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (Bitmap bitmap : mBitmaps) {
            canvas.drawBitmap(bitmap, 0, 0, null);
            canvas.translate(bitmap.getWidth() / 2, 0);
        }
    }
}
複製程式碼

但是,如果我們開啟除錯GPU過度繪製的開關,那麼可以得到下面的檢測結果,可以發現在兩張圖片重疊的地方,會出現明顯的過度繪製:

效能優化技巧知識梳理(1)   佈局優化
而如果,我們採用ClipRectonDraw方法進行優化:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        int bits = mBitmaps.length;
        for (int i = 0; i < bits; i++) {
            Bitmap bitmap = mBitmaps[i];
            int bitW = bitmap.getWidth();
            int bitH = bitmap.getHeight();
            if (i != 0) {
                canvas.translate(bitW / 2, 0);
            }
            canvas.save();
            if (i != bits - 1) {
                canvas.clipRect(0, 0, bitW / 2, bitH);
            }
            canvas.drawBitmap(bitmap, 0, 0, null);
            canvas.restore();
        }
        canvas.restore();
    }
複製程式碼

此時,檢測的結果如下,和上圖相比,我們很好地解決了過度繪製的問題:

效能優化技巧知識梳理(1)   佈局優化

(11) 使用 AsyncLayoutInflater 非同步載入佈局

Android Support Library 24中,提供了一個AsyncLayoutInflater工具類用於實現xml佈局的非同步inflate,它的用法和普通的LayoutInflater類似,只不過它inflate的執行是在子執行緒當中,當這一過程完成之後,再通過OnInflateFinishedListener介面,回撥到主執行緒當中。

首先是整個Activity的根佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/tv_async"
        android:text="開始非同步 Inflate 佈局"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>
</LinearLayout>
複製程式碼

接下來是需要非同步inflate的子佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:text="非同步 Inflate 的佈局"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"/>
</LinearLayout>
複製程式碼

使用方式如下:

    private void asyncInflated() {
        TextView textView = (TextView) findViewById(R.id.tv_async);
        final ViewGroup root = (ViewGroup) findViewById(R.id.ll_root);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AsyncLayoutInflater asyncLayoutInflater = new AsyncLayoutInflater(OptActivity.this);
                asyncLayoutInflater.inflate(R.layout.layout_async, root, new AsyncLayoutInflater.OnInflateFinishedListener() {

                    @Override
                    public void onInflateFinished(View view, int resId, ViewGroup parent) {
                        parent.addView(view);
                    }
                });
            }
        });
    }
複製程式碼

inflate方法接收三個引數:

  • 需要非同步inflate的佈局id
  • 所需要新增到的根佈局的例項。
  • 非同步inflate完成的回撥,該回撥是在主執行緒當中執行。需要注意,在該回撥執行時,非同步inflate出來的佈局並沒有新增到父佈局當中,因此,我們需要通過addView的方法將其新增到View樹當中。

最終的執行結果為:

效能優化技巧知識梳理(1)   佈局優化

(12) 使用效能檢測工具,找出佈局中的效能瓶頸

在分析佈局有可能導致的效能問題時,我們一般會用到以下幾種工具,這些工具我們在之前學習效能優化工具的時候都有接觸過:


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章