使用ViewStub來提高載入效能吧!
什麼是ViewStub?
ViewStub其實本質上也是一個View,其繼承關係如圖所示:
為什麼ViewStub可以提高載入效能?
ViewStub使用的是惰性載入的方式,即使將其放置於佈局檔案中,如果沒有進行載入那就為空,不像其它控制元件一樣只要佈局檔案中宣告就會存在。
那ViewStub適用於場景呢?通常用於網路請求頁面失敗的顯示。一般情況下若要實現一個網路請求失敗的頁面,我們是不是使用兩個View呢,一個隱藏,一個顯示。試想一下,如果網路狀況良好,並不需要載入失敗頁面,但是此頁面確確實實已經載入完了,無非只是隱藏看不見而已。如果使用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就行啦,就是介個:
感興趣的童鞋可以自己去試試。
原始碼分析
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此時並未銷燬,所以建議初始化後將其設定為空。