利用ViewStub实现布局懒惰加载

这个问题也是头条面试官问的,本身没什么难度,但以前确实没仔细研究过。

1、使用介绍

ViewStub是一种不可见的尺寸为0的View,用来实现布局资源的懒加载。当ViewStub被设置为用户可见或其  inflate() 被调用时,实际的布局资源才会被加载。这时ViewStub在View树中的位置会被新加载的View取代,并且新加载的View会继承ViewStub所拥有的布局参数。而且我们还可以通过ViewStub的 inflatedId 属性定义新加载View的控件ID。

<ViewStub 
      android:id="@+id/stub"
      android:inflatedId="@+id/subTree"
      android:layout="@layout/mySubTree"
      android:layout_width="120dip"
      android:layout_height="40dip" />

此时ViewStub可以通过“stub”这个ID获取到,在“mySubTree”指向的布局被加载之后,ViewStub会被从View树中移出,“myStubTree”指向的布局便可以通过“subTree”这个ID获取到。新加载的布局会继承ViewStub的布局参数,在这里就是宽120dip,高40dip。利用StubView加载布局的方式如下:

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

 inflate() 被调用以后,ViewStub被新加载的View取代。通过这种方式应用可以直接获取到新加载的View而无需再次调用  findViewById() 。

翻译的可能不是太准确,有兴趣的朋友可以看原文:StubView

2、源码分析

显然,我们需要关注的函数是ViewStub的 inflate() 。这个函数在使用的时候有一个需要注意的地方就是它只能被调用一次,至于为什么可以在源码中找到答案。

public View inflate() {
    // 获取父View。
    final ViewParent viewParent = getParent();
    // 如果父View为null或者父View不是ViewGroup的子类则抛出异常。
    if (viewParent != null && viewParent instanceof ViewGroup) {
        // mLayoutResource即为StubView的layout属性的值,它代表了
        // 需要被加载的布局ID。显然它不能为0。
        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);
            // 此处StubView将自己的inflatedId属性的值设置给了新加载的View。
            if (mInflatedId != NO_ID) {
                view.setId(mInflatedId);
            }
            // 紧接着StubView将自己从父View中移出。
            final int index = parent.indexOfChild(this);
            parent.removeViewInLayout(this);
            // 将自己的LayoutParams设置给新加载的View,这就是为什么新加载的View会
            // 继承ViewStub的布局参数。
            final ViewGroup.LayoutParams layoutParams = getLayoutParams();
            // 将新加载的View添加到父View中,而且是自己原来的位置。
            if (layoutParams != null) {
                parent.addView(view, index, layoutParams);
            } else {
                parent.addView(view, index);
            }
            // 保存新加载的View的弱引用。
            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");
    }
}

在函数的第26行可以看到,ViewStub将自己从父View中移除,所以如果再次调用 inflate() ,第5行的判断不成立就会抛出异常。在第37行,ViewStub保存了新加载的View的弱引用,为什么还要保存这个引用呢?这是因为在调用了 inflate() 之后,仍可以使用StubView的 setVisibility() 来设置新加载View的可见性。

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();
        }
    }
}

如果之前已将调用了 inflate() 那么 mInflatedViewRef 肯定不为 null ,否则如果参数为 VISIBLE 或 INVISIBLE 的话, inflate() 函数就会被调用。

3、总结

ViewStub如何提高我们的加载性能:

在它的初始化函数中,使用了 setVisibility(GONE) ,我们知道,可见性为 GONE 的View是不会被绘制且不占用空间的。

与直接将目标控件的可见性设置为 GONE 相比它的优势:

即使将可见性设置为 GONE ,在加载布局时仍需要对控件进行初始化等操作。这时ViewStub则显得十分轻量。

posted @ 2018-03-21 15:24  mmmmar  阅读(894)  评论(0编辑  收藏  举报