ViewStub学习

概述

一提到ViewStub,可能很多人就会想到布局性能优化,那么它具体是怎么实现的呢?

先看下官方的介绍

A ViewStub is an invisible, zero-sized View that can be used to lazily inflate layout resources at runtime. When a ViewStub is made visible, or when inflate() is invoked, the layout resource is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views. Therefore, the ViewStub exists in the view hierarchy until setVisibility(int) or inflate() is invoked. The inflated View is added to the ViewStub’s parent with the ViewStub’s layout parameters. Similarly, you can define/override the inflate View’s id by using the ViewStub’s inflatedId property

翻译如下:

  • ViewStub是个不可见的、大小为0的View,用于在运行过程中延迟加载布局。
  • 当ViewStub被设置为可见或者inflate()方法被调用,该布局才会被加载,之后ViewStub自身会被所加载的布局替换掉,ViewStub就不再存在于视图层级中。
  • 被加载布局会使用ViewStub的布局参数,我们可以定义或者重写被加载的布局id通过ViewStub的inflatedId属性。

还是比较好理解的,官方比较推崇的用于获取加载的布局的写法如下:

1
2
ViewStub stub = findViewById(R.id.stub);
View inflated = stub.inflate();

下面通过一个例子来验证上面这些概念。

例子

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends Activity {
Button button;
ViewStub viewStub;
View inflateView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button);
viewStub = (ViewStub) findViewById(R.id.stub);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
inflateView = viewStub.inflate();
}
});
}
}

view_stub.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button1" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button2" />
</LinearLayout>

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.huchengyang.viewstub.MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button1" />
<ViewStub
android:id="@+id/stub"
android:inflatedId="@+id/subTree"
android:layout="@layout/view_stub"
android:layout_height="wrap_content"
android:layout_width="match_parent" />
</LinearLayout>

代码很简单,运行效果如下:

通过Hierarchy View查看此时的布局层级如下:

点击Button1后:

Hierarchy View查看布局层级如下:

根据布局层级的显示确实符合官网的描述,当调用inflate()方法后,在布局层级中ViewStub就被代替了。在接触ViewStub之前我一直有个疑问,要想控制布局显示与否,通过设置Visibility属性不就好了么?我们改下activity_main的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.huchengyang.viewstub.MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button1" />
<!-- <ViewStub
android:id="@+id/stub"
android:inflatedId="@+id/subTree"
android:layout="@layout/view_stub"
android:layout_height="wrap_content"
android:layout_width="match_parent" />-->
<LinearLayout
android:id="@+id/linear"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button1" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button2" />
</LinearLayout>
</LinearLayout>

运行如下:

再看下布局层级:

View的visibility属性设置成gone虽然在界面上看不到,但是布局却已经被加载进去了,如果该布局内容很复杂,那么整体的布局性能就会下降,而使用ViewStub就能延迟加载,提高布局性能。

注意

官网的提醒:

Note: One drawback of ViewStub is that it doesn’t currently support the <merge> tag in the layouts to be inflated.

指出当前ViewStub不支持标签。修改下view_stub.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?>
<merge>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button1" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button2" />
</LinearLayout>
</merge>

点击button1后得到如下崩溃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FATAL EXCEPTION: main
Process: com.example.huchengyang.viewstub, PID: 6174
android.view.InflateException: Binary XML file line #2: <merge /> can be used only with a valid ViewGroup root and attachToRoot=true
at android.view.LayoutInflater.inflate(LayoutInflater.java:539)
at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
at android.view.ViewStub.inflate(ViewStub.java:259)
at com.example.huchengyang.viewstub.MainActivity$1.onClick(MainActivity.java:23)
at android.view.View.performClick(View.java:5198)
at android.view.View$PerformClick.run(View.java:21147)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

可见,ViewStub确实不支持merge标签。

源码分析

前面说过通过setVisibility()和inflate()方法可以实现延迟加载,那么它具体是怎么实现的呢,有必要看下源码(以下代码分析基于android-25)。不看不知道,一看吓一跳!整个ViewStub的源码一共才321行,而且绿油油的注释就占了一半,所以我们完全可以自己撸一个XXXViewStub!

首先看下ViewStub的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

可以看到初始化的时候执行了setVisibility(GONE),ViewStub重写了该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
* {@link #inflate()} is invoked and this StubbedView is replaced in its parent
* by the inflated layout resource. After that calls to this function are passed
* through to the inflated view.
*
* @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
*
* @see #inflate()
*/
@Override
@android.view.RemotableViewMethod
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
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) {
inflate();
}
}
}

这里的mInflatedViewRef是个弱引用的View,它的初始化在inflate()中,所以当visibility为GONE时,调用View的setVisibility就结束了;当我们设置该属性为VISIBLE时,执行inflate(),看下该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Inflates the layout resource identified by {@link #getLayoutResource()}
* and replaces this StubbedView in its parent by the inflated layout resource.
*
* @return The inflated layout resource.
*
*/
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
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);
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
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);
}
mInflatedViewRef = new WeakReference<View>(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");
}
}

代码也很好懂,首先获取ViewStub所在的父布局,父布局必须不为空且所要加载的布局是个ViewGroup类型才往下走,mLayoutResource就是我们在xml里指定的android:layout,如果没配置也会报错,这些都满足后就会通过LayoutInflater去加载布局,设置布局id,也就是我们在xml里指定的android:inflatedId,然后找到ViewStub,并把它从布局里删掉,再根据ViewStub的布局参数,将所以加载的布局加到父布局中,并且对弱引用初始化。因此,如果重复调用ViewStub的inflate()会报错,但是重复调用setVisibility()不会。

加个鸡腿