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

1、前言

性能優化包含的部分不少,包括佈局、內存、耗電、流量等等,其中佈局優化是最容易掌握,也最容易被你們所忽視的一個方面,今天,就來介紹一下有關佈局優化的一些技巧。android

2、佈局優化技巧

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

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

根佈局爲:

<?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>
複製代碼

要點bash

  • 直接在根佈局中,若是但願找到<include>所指定的layout中包含的控件,那麼就須要給<include>指定id,再經過它來尋找子容器中的控件。
  • <include>標籤中,能夠指定layout_xxx屬性,它將會覆蓋子佈局中的根標籤中的屬性。

(2) 使用 標籤減小布局層級

當出現下面這種狀況:一個xml佈局文件的根節點是一個FrameLayout,而且它沒有一個有用的背景,那麼當該xml佈局文件渲染出的ViewGroup被添加到父佈局中時,鏈接處就會出現一個多餘的節點,而採用<merge>標籤能夠去掉這一無用節點,從而下降佈局的層級。異步

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

假如咱們沒有使用 <merge>標籤,那麼情形爲:
要點

  • 當須要經過LayoutInflaterinflate方法渲染出以<merge>做爲根節點標籤的xml文件時,必須傳入不爲nullroot參數,且attachToRoot參數必須爲true
  • <merge>只可做爲xml的根節點。
  • <merge>既不是View也不是ViewGroup,它只是表示一組等待被添加的視圖,所以,對它設定的任何屬性都是無用的。

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

當咱們的佈局中,存在一些須要按序加載的控件,那麼就可使用ViewStub標籤預先聲明,當狀況知足時再去實例化ViewStub中所聲明的佈局,其用法以下:ide

  • 首先,在佈局中預先聲明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所指定的值:

(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);
    }
複製代碼

最終能夠實現以下的效果:

除此以外,還能夠實現圖文混排,例以下面這樣:

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

例以下面的佈局:

此時咱們就可使用 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當中:

這時候,就能夠經過 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的方式來實現,例以下面的效果:

其佈局以下所示:

<?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>
複製代碼

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

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

此時的檢測結果以下,能夠看到,根佈局就不存在過分繪製的狀況了:

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

當咱們在自定義控件,並重寫onDraw方法來完成相應的需求時,一些錯誤的操做每每會致使佈局效率的下降,通常來講,有兩點須要注意:

  • 避免在其中進行對象的分配
  • 使用CanvasClipRect方法避免過分繪製

這裏用一個簡單的例子來講明一下第二點的實現,當咱們須要實現下面這個多張圖片重疊的自定義控件時:

假如咱們直接使用下面的方式,也能夠實現上面的效果:

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過分繪製的開關,那麼能夠獲得下面的檢測結果,能夠發如今兩張圖片重疊的地方,會出現明顯的過分繪製:

而若是,咱們採用 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();
    }
複製代碼

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

(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樹當中。

最終的運行結果爲:

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

在分析佈局有可能致使的性能問題時,咱們通常會用到如下幾種工具,這些工具咱們在以前學習性能優化工具的時候都有接觸過:


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

相關文章
相關標籤/搜索