1. 程式人生 > >使用ViewStub來提高載入效能吧!

使用ViewStub來提高載入效能吧!

什麼是ViewStub?

ViewStub其實本質上也是一個View,其繼承關係如圖所示:
ViewStub繼承關係

為什麼ViewStub可以提高載入效能?

ViewStub使用的是惰性載入的方式,即使將其放置於佈局檔案中,如果沒有進行載入那就為空,不像其它控制元件一樣只要佈局檔案中宣告就會存在。
那ViewStub適用於場景呢?通常用於網路請求頁面失敗的顯示。一般情況下若要實現一個網路請求失敗的頁面,我們是不是使用兩個View呢,一個隱藏,一個顯示。試想一下,如果網路狀況良好,並不需要載入失敗頁面,但是此頁面確確實實已經載入完了,無非只是隱藏看不見而已。如果使用ViewStub,在需要的時候才進行載入,不就達到節約記憶體提高效能的目的了嗎?

ViewStub的載入原理

ViewStub只能載入一次,重複載入會導致異常,這是因為ViewStub只要載入過一次,其自身就會被移除,把並自身所包含的內容全部傳給父佈局。來張圖感受一下
ViewStub的載入原理

ViewStub如何使用

  • 父佈局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id
="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop
="@dimen/activity_vertical_margin" android:orientation="vertical" tools:context="com.example.administrator.myviewstub.MainActivity">
<Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="inflate" android:text="inflate" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="setData" android:text="setdata"/> <ViewStub android:id="@+id/vs" android:layout="@layout/viewstub" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>

可以看到ViewStub又引用了另外一個佈局。

  • ViewStub佈局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/hello_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="DATA EMPTY!"/>
</FrameLayout>
  • MainActivity
public class MainActivity extends AppCompatActivity {
    private ViewStub viewStub;
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewStub = (ViewStub) findViewById(R.id.vs);
        //textView  = (TextView) findViewById(R.id.hello_tv);空指標
    }
    public  void inflate(View view){
        viewStub.inflate();
        //textView  = (TextView) viewStub.findViewById(R.id.hello_tv);空指標
        textView  = (TextView) findViewById(R.id.hello_tv);
    }
    public void setData(View view){
        textView.setText("DATA!!");

    }
}

注意:這裡有幾個坑,前面我們說了ViewStub只有在初始化之後才會存在,所以第一個註釋中的空指標是因為ViewStub還未初始化。那第二個註釋中的空指標是怎麼回事呢?前面我們也說了,ViewStub在載入完成之後會移除自身,並把自身的內容轉給父佈局,所以此時viewStub中的內容已經不存在了,textView已經是父佈局的東西了,所以不能使用viewStub來findViewById。另外,前面我們說了ViewStub只能載入一次,若呼叫兩次inflate()的話會導致異常。
載入異常

為了驗證所得出的結論是不是正確的,我截了兩張ViewStub載入前後的圖。

  • 載入前:
    載入前

從圖中可以看到ViewStub還沒載入,是灰色的。

  • 載入後:
    載入後

當點選了INFLATE之後,可以看到,ViewStub消失了,取而代之的是一個FrameLayout,其中包含了一個AppCompatTextView(ps.其實就是TextView,只是Google在5.0之後提供了向前相容,就好比AppCompatActivity和Activity一樣)。咳,這個FrameLayout是不是很眼熟,沒錯!就是我們之前寫的ViewStub的佈局,忘記的童鞋翻回去看看。

  • 順便驗證一下TextView是不是能用,點選SETDATA:
    這裡寫圖片描述

沒有問題。

關於這個圖是怎麼來的,童鞋們只要點選Android Montior中的Layout Inspector就行啦,就是介個:
Layout Inspector
感興趣的童鞋可以自己去試試。

原始碼分析

ViewStub是如何實現的呢,接下來我們來一探究竟:

 public View inflate() {
        final ViewParent viewParent = getParent();//獲取父View

        if (viewParent != null && viewParent instanceof ViewGroup) {
        //若父不是ViewGroup就會丟擲異常("ViewStub must have a non-null ViewGroup viewParent")
            if (mLayoutResource != 0) {
            //這個就是ViewStub只能載入一次的原因,第二次載入則丟擲異常(throw new IllegalArgumentException("ViewStub must have a valid layoutResource"))
                final ViewGroup parent = (ViewGroup) viewParent;
                final LayoutInflater factory;
                if (mInflater != null) {
                    factory = mInflater;
                } else {
                    factory = LayoutInflater.from(mContext);
                }
                final View view = factory.inflate(mLayoutResource, parent,
                        false);
                //建立一個View,這個View為ViewStub的內容,mLayoutResource為ViewStub自身的Layout資原始檔id
                if (mInflatedId != NO_ID) {
                    view.setId(mInflatedId);
                    //若mInflatedId存在,則將id重新賦值給新的View
                }

                final int index = parent.indexOfChild(this);
                parent.removeViewInLayout(this);
                //通過父佈局將ViewStub移除
                final ViewGroup.LayoutParams layoutParams = getLayoutParams();
                if (layoutParams != null) {
                    parent.addView(view, index, layoutParams);
                } else {
                    parent.addView(view, index);
                }
                //將ViewStub中的內容新增到父容器中
                mInflatedViewRef = new WeakReference<View>(view);
                //設定為弱引用,當VIewStub設定為空時將立即執行GC
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
                //最後將View返回出去
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

ViewStub中還有一個setVisibility(int visibility)值得我們注意:

public void setVisibility(int visibility) {
        if (mInflatedViewRef != null) {
            View view = mInflatedViewRef.get();
            //拿到關聯的Layout
            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) {
                inflate();
            }
        }
    }

可以看到,ViewStub若之前沒有進行inflate,setVisibility(View.VISIBLE)或setVisibility(View.INVISIBLE)會自動先進行載入,而載入之後可以設定顯示和隱藏。並且ViewStub設定的不是自己,而是拿到關聯的那個Layout設定visible。ViewStub此時並未銷燬,所以建議初始化後將其設定為空。