Android UI性能優(yōu)化——ViewStub和Merge的使用

ViewStub

簡介

ViewStub 是一種沒有任何維度的輕量型視圖,它不會(huì)繪制任何內(nèi)容或參與布局。

  • ViewStub是一種沒有大小,不占用布局的View。
  • 直到當(dāng)調(diào)用 inflate() 方法或者可見性變?yōu)閂ISIBLE時(shí),才會(huì)將指定的布局加載到父布局中。
  • ViewStub加載完指定布局之后會(huì)被移除,不再占用空間。(所以 inflate() 方法只能調(diào)用一次 )

因?yàn)檫@些特性ViewStub可以用來懶加載布局,優(yōu)化UI性能。

使用

布局

在布局中添加ViewStub標(biāo)簽并通過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" />

代碼

在需要展示布局的地方調(diào)用 inflate() 方法或者將ViewStub的可見性設(shè)置為VISIBLE。

private View viewStubContentView = null;

visibleViewStub.setVisibility(View.VISIBLE);

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

注意inflate() 方法只能調(diào)用一次,重復(fù)調(diào)用被拋出IllegalStateException異常。

inflate() 方法會(huì)返回替換的布局的根View而設(shè)置VISIBLE不會(huì)返回,如果需要獲取替換布局的實(shí)例,如:需要為替換的布局設(shè)置監(jiān)聽事件,這是需要使用inflate() 方法而不是VISIBLE。

ViewStub源碼分析

針對(duì)我們前面說的ViewStub的幾個(gè)特點(diǎn),我們來分析下源碼是如何實(shí)現(xiàn)的。分析源碼可以學(xué)習(xí)別人優(yōu)秀的代碼設(shè)計(jì),也可以為我們?nèi)蘸箢愃菩枨蟮膶?shí)現(xiàn)提供借鑒。

ViewSutb沒有大小,不占用布局

ViewStub在構(gòu)造方法中設(shè)置了控件可見性為GONE并且指定不進(jìn)行繪制。

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();
    //設(shè)置不可見
    setVisibility(GONE);
    //指定不進(jìn)行繪制
    setWillNotDraw(true);
}

并且重寫了onMeasure(widthMeasureSpec, heightMeasureSpec)設(shè)置尺寸為(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()方法

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

@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        //如果弱引用不為空且View不為空,調(diào)用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) {
            //弱引用為空且可見性設(shè)置為VISIBLE或者INVISIBLE,調(diào)用inflate()方法
            inflate();
        }
    }
}

到這里基本可以分析出弱引用持有的對(duì)象就是替換布局的View。繼續(xù)往下看mInflatedViewRef是在哪里初始化的。

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

public View inflate() {
    //獲取父布局
    final ViewParent viewParent = getParent();
    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            //獲取要替換的View對(duì)象
            final View view = inflateViewNoAdd(parent);
            //執(zhí)行替換操作
            replaceSelfWithView(view, parent);
            //初始化弱引用持有View對(duì)象
            mInflatedViewRef = new WeakReference<>(view);
            if (mInflateListener != null) {
                //觸發(fā)監(jiān)聽
                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對(duì)象并執(zhí)行了替換操作,mInflatedViewRef持有的確實(shí)是替換View對(duì)象的實(shí)例。

ViewStub加載完指定布局之后會(huì)被移除,不再占用空間

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

private View inflateViewNoAdd(ViewGroup parent) {
    final LayoutInflater factory;
    if (mInflater != null) {
        factory = mInflater;
    } else {
        factory = LayoutInflater.from(mContext);
    }
    //動(dòng)態(tài)加載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()執(zhí)行了移除和替換兩步操作。這也解釋了為什么inflate()方法只能執(zhí)行一次,因?yàn)閳?zhí)行replaceSelfWithView()自身已經(jīng)被移除,再次執(zhí)行inflate()方法獲取getParent()會(huì)為空,從而拋出IllegalStateException異常。

使用場景

app頁面中總會(huì)有一些布局是不常顯示的,如一些特殊提示和頁面loading等,這時(shí)可以使用ViewStub來實(shí)現(xiàn)懶加載的功能,優(yōu)化UI性能。

總結(jié)

ViewStub雖然實(shí)現(xiàn)簡單,但是源碼設(shè)計(jì)巧妙。對(duì)于頁面中的不常用布局使用ViewSutb懶加載有一定的優(yōu)化效果。

Merge

簡介

  • merge既不是View也不是ViewGroup,只是一種標(biāo)記。
  • merge必須在布局的根節(jié)點(diǎn)。
  • 當(dāng)merge所在布局被添加到容器中時(shí),merge節(jié)點(diǎn)被合并不占用布局,merge下面的所有視圖轉(zhuǎn)移到容器中。

使用

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

不使用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="測試標(biāo)題"/>
</RelativeLayout>

實(shí)際Activity布局層級(jí):

<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="測試標(biāo)題"/>
        </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進(jìn)行優(yōu)化:

優(yōu)化后的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="測試標(biāo)題"/>
</merge>

使用tools:parentTag屬性可以指定父布局類型,方便在Android Studio中編寫布局時(shí)進(jìn)行預(yù)覽。

實(shí)際Activity布局層級(jí):

<?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="測試標(biāo)題"/>

    </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之后布局層級(jí)減少了一層。

使用場景

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

來看一種使用頻率更高的應(yīng)用場景——自定義View,大家應(yīng)該都實(shí)現(xiàn)過,比如要定義一個(gè)通用的天氣控件,通常是自定義一個(gè)WeatherView 繼承自RelativeLayout,然后通過inflate動(dòng)態(tài)引入布局,那么布局怎么寫呢?不使用merge的情況下根布局肯定是RelativeLayout,引入WeatherView之后豈不是嵌套了一層RelativeLayout。這時(shí)候就可以在布局中使用merge進(jìn)行優(yōu)化。

還有一種應(yīng)用場景,如果Activity的根布局是FrameLayout可以使用merge進(jìn)行替換,使用之后可以使Activity的布局層級(jí)減少一層。為什么會(huì)這樣呢?首先我們要了解Activity頁面的布局層級(jí),最外層是PhoneWindow其下是一個(gè)DecorView下面就是TitleView和ContentView,ContentView就是我們通過SetContentView設(shè)置的Activity的布局,沒錯(cuò)ContentView是一個(gè)FrameLayout,所以在Activity布局中使用merge可以減少層級(jí)。

總結(jié)

正確的使用merge可以有效的減少布局層級(jí),提高頁面渲染速度。但是merge使用限制比較多,應(yīng)用場景比較少。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容