Android UI性能優化——ViewStub和Merge的使用

ViewStub的使用

簡介

ViewStub 是一種沒有任何維度的輕量型視圖,它不會繪製任何內容或參與佈局。

  • ViewStub是一種沒有大小,不佔用佈局的View。
  • 直到當調用 inflate() 方法或者可見性變爲VISIBLE時,纔會將指定的佈局加載到父佈局中。
  • ViewStub加載完指定佈局之後會被移除,不再佔用空間。(所以 inflate() 方法只能調用一次 )

因爲這些特性ViewStub可以用來懶加載佈局,優化UI性能。

使用

佈局

在佈局中添加ViewStub標籤並通過layout屬性指定要替換的佈局。

<ViewStub
      android:id="@+id/visible_view_stub"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout="@layout/layout_view_stub_content" />

代碼

在需要展示佈局的地方調用 inflate() 方法或者將ViewStub的可見性設置爲VISIBLE。

private View viewStubContentView = null;

visibleViewStub.setVisibility(View.VISIBLE);

if(viewStubContentView == null){
    viewStubContentView = inflateViewStub.inflate();
}

注意inflate() 方法只能調用一次,重複調用被拋出IllegalStateException異常。

inflate() 方法會返回替換的佈局的根View而設置VISIBLE不會返回,如果需要獲取替換佈局的實例,如:需要爲替換的佈局設置監聽事件,這是需要使用inflate() 方法而不是VISIBLE。

ViewStub源碼分析

針對我們前面說的ViewStub的幾個特點,我們來分析下源碼是如何實現的。分析源碼可以學習別人優秀的代碼設計,也可以爲我們日後類似需求的實現提供借鑑。

ViewSutb沒有大小,不佔用佈局

ViewStub在構造方法中設置了控件可見性爲GONE並且指定不進行繪製。

public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context);
    final TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.ViewStub, defStyleAttr, defStyleRes);
    mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
    mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
    mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
    a.recycle();
    //設置不可見
    setVisibility(GONE);
    //指定不進行繪製
    setWillNotDraw(true);
}

並且重寫了onMeasure(widthMeasureSpec, heightMeasureSpec)設置尺寸爲(0,0),並且重寫了draw(canvas)dispatchDraw(canvas)方法,並且沒有做任何繪製操作。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //指定尺寸爲0,0
    setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
@Override
protected void dispatchDraw(Canvas canvas) {
}

setVisibility()inflate()方法

//定義了一個View的弱引用
private WeakReference<View> mInflatedViewRef;

@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        //如果弱引用不爲空且View不爲空,調用View的setVisibility方法
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            //弱引用爲空且可見性設置爲VISIBLE或者INVISIBLE,調用inflate()方法
            inflate();
        }
    }
}

到這裏基本可以分析出弱引用持有的對象就是替換佈局的View。繼續往下看mInflatedViewRef是在哪裏初始化的。

inflate()方法,核心方法執行具體的佈局替換操作。

public View inflate() {
    //獲取父佈局
    final ViewParent viewParent = getParent();
    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            //獲取要替換的View對象
            final View view = inflateViewNoAdd(parent);
            //執行替換操作
            replaceSelfWithView(view, parent);
            //初始化弱引用持有View對象
            mInflatedViewRef = new WeakReference<>(view);
            if (mInflateListener != null) {
                //觸發監聽
                mInflateListener.onInflate(this, view);
            }
            return view;
        } else {
            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
        }
    } else {
        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
}

inflate()方法中獲取要替換的View對象並執行了替換操作,mInflatedViewRef持有的確實是替換View對象的實例。

ViewStub加載完指定佈局之後會被移除,不再佔用空間

我們繼續來看inflateViewNoAdd() 方法和replaceSelfWithView()方法。

private View inflateViewNoAdd(ViewGroup parent) {
    final LayoutInflater factory;
    if (mInflater != null) {
        factory = mInflater;
    } else {
        factory = LayoutInflater.from(mContext);
    }
    //動態加載View
    final View view = factory.inflate(mLayoutResource, parent, false);
    if (mInflatedId != NO_ID) {
        view.setId(mInflatedId);
    }
    return view;
}

inflateViewNoAdd() 方法比較簡單,沒什麼好解釋的。

private void replaceSelfWithView(View view, ViewGroup parent) {
    final int index = parent.indexOfChild(this);
    //從父佈局中移除自己
    parent.removeViewInLayout(this);
    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    if (layoutParams != null) {
        //添加替換佈局
        parent.addView(view, index, layoutParams);
    } else {
        //添加替換佈局
        parent.addView(view, index);
    }
}

replaceSelfWithView()執行了移除和替換兩步操作。這也解釋了爲什麼inflate()方法只能執行一次,因爲執行replaceSelfWithView()自身已經被移除,再次執行inflate()方法獲取getParent()會爲空,從而拋出IllegalStateException異常。

使用場景

app頁面中總會有一些佈局是不常顯示的,如一些特殊提示和頁面loading等,這時可以使用ViewStub來實現懶加載的功能,優化UI性能。

總結

ViewStub雖然實現簡單,但是源碼設計巧妙。對於頁面中的不常用佈局使用ViewSutb懶加載有一定的優化效果。


Merge的使用

簡介

  • merge既不是View也不是ViewGroup,只是一種標記。
  • merge必須在佈局的根節點。
  • 當merge所在佈局被添加到容器中時,merge節點被合併不佔用佈局,merge下面的所有視圖轉移到容器中。

使用

通過一種比較常用的場景來比較下使用merge和不使用的區別。

不使用merge

Activity佈局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:id="@+id/title_rel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <include layout="@layout/layout_merge"/>
        
    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/content_rel"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/title_rel"/>
</RelativeLayout>

ToolBar佈局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools" >

    <ImageView
        android:id="@+id/home_iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_below="@id/home_iv"
        android:gravity="center_vertical"
        android:textSize="25sp"
        android:textColor="#000000"
        tools:text="測試標題"/>
</RelativeLayout>

實際Activity佈局層級:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:id="@+id/title_rel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <RelativeLayout 
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >

            <ImageView
                android:id="@+id/home_iv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:src="@mipmap/ic_launcher"/>

            <TextView
                android:id="@+id/title_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@id/home_iv"
                android:gravity="center_vertical"
                android:textSize="25sp"
                android:textColor="#000000"
                tools:text="測試標題"/>
        </RelativeLayout>

    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/content_rel"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/title_rel"/>
</RelativeLayout>

使用merge進行優化:

優化後的ToolBar佈局:

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools"
    tools:parentTag="android.widget.RelativeLayout">

    <ImageView
        android:id="@+id/home_iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_below="@id/home_iv"
        android:gravity="center_vertical"
        android:textSize="25sp"
        android:textColor="#000000"
        tools:text="測試標題"/>
</merge>

使用tools:parentTag屬性可以指定父佈局類型,方便在Android Studio中編寫佈局時進行預覽。

實際Activity佈局層級:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:id="@+id/title_rel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/home_iv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:src="@mipmap/ic_launcher"/>

        <TextView
            android:id="@+id/title_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_below="@id/home_iv"
            android:gravity="center_vertical"
            android:textSize="25sp"
            android:textColor="#000000"
            tools:text="測試標題"/>

    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/content_rel"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/title_rel"/>
</RelativeLayout>

可以看到使用merge之後佈局層級減少了一層。

使用場景

上面例子可能不太合適,這麼寫佈局容易被打。

來看一種使用頻率更高的應用場景——自定義View,大家應該都實現過,比如要定義一個通用的天氣控件,通常是自定義一個WeatherView 繼承自RelativeLayout,然後通過inflate動態引入佈局,那麼佈局怎麼寫呢?不使用merge的情況下根佈局肯定是RelativeLayout,引入WeatherView之後豈不是嵌套了一層RelativeLayout。這時候就可以在佈局中使用merge進行優化。

還有一種應用場景,如果Activity的根佈局是FrameLayout可以使用merge進行替換,使用之後可以使Activity的佈局層級減少一層。爲什麼會這樣呢?首先我們要了解Activity頁面的佈局層級,最外層是PhoneWindow其下是一個DecorView下面就是TitleView和ContentView,ContentView就是我們通過SetContentView設置的Activity的佈局,沒錯ContentView是一個FrameLayout,所以在Activity佈局中使用merge可以減少層級。

總結

正確的使用merge可以有效的減少佈局層級,提高頁面渲染速度。但是merge使用限制比較多,應用場景比較少。

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