ViewStub 是如何实现懒加载的

3,067 阅读5分钟

ViewStub简介

public final class ViewStub extends View 

ViewStub 是一个宽高都为0,不可见的(GONE),不参与measure与layout(绝大部分情况),不绘制任何东西,可以用来做懒加载的View,常用于布局优化;

PS: 为什么说绝大部分情况不参与测量与布局呢?因为大部分ViewGroup对于GONE的View,都不会让它参与测量与布局流程(自定义的就不一定了,另外可以看一下FrameLayout的源码)。

首先需要说的是,本文涉及到两个角色,一个是 ViewStub本身,另外一个是被用来做懒加载的View,是ViewStub的作用对象,称之为『StubbedView』(本文用此称呼来替代)。

那么 ViewStub 是怎么实现懒加载的呢?

本文通过ViewStub源码来分析。

ViewStub的简单使用教程

ViewStub 的使用非常非常简单,只需要两步~

Step 1. 在XML里配置使用:

Step 2. 调用ViewStub的inflate

ViewStub stub = (ViewStub)findViewById(R.id.stub);
View stubbedView = stub.inflate();//后面分析
//...初始化StubbedView

非常简单的两步,就能做到View的懒加载,非常方便,其原因是什么呢?

接下去深入源码分析一下。

构造方法分析

首先分析一下构造方法,了解一下它是如何创建的。

public ViewStub(Context context, @LayoutRes int layoutResource) {
    this(context, null);
    // StubbedView的资源id
    mLayoutResource = layoutResource;
}
public ViewStub(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context);
    final TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.ViewStub, defStyleAttr, defStyleRes);
    // mInflatedId 存储StubbedView的id
    mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
    // mLayoutResource 为StubbedView的resourceId
    mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
    // viewStub 自己的id
    mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
    a.recycle();
    // 设置为不可见 
    setVisibility(GONE);
    // 不绘制本身
    setWillNotDraw(true);
}

ViewStub在构造方法里不仅仅获取赋值属性,比较关键的是,还 默认将ViewStub自己设置为不可见(跳过onMeasure与onLayout),不绘制。

这里有一个要点:在XML里配置ViewStub的可见性是没有用的

测量 与 绘制

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 写死的宽高为0
    setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
    //空方法,不draw任何东西
}
@Override
protected void dispatchDraw(Canvas canvas) {
    //空方法,不draw任何东西
}

inflate()方法分析

之前在简单教程里有提到 inflate方法,它是ViewStub实现懒加载的最为关键的方法,接下去去分析一下。

// 返回 StubbedView
public View inflate() {
    // 尝试去获取 viewParent 第一次调用的时候不为null,而后则为null
    final ViewParent viewParent = getParent();
    // 当 viewParent 不为null的时候
    if (viewParent != null && viewParent instanceof ViewGroup) {
        // 我们在xml里配置的layout的资源id 如果id无效,则会报错
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            // 实例化 LayoutInflater
            final LayoutInflater factory;
            if (mInflater != null) {
                factory = mInflater;
            } else {
                factory = LayoutInflater.from(mContext);
            }
            // inflate,StubbedView在这里被实例化
            final View view = factory.inflate(mLayoutResource, parent,
                    false);
            // 可以看到,这里如果我们在XML里写了inflateId,则会设置给StubbedView
            if (mInflatedId != NO_ID) {
                view.setId(mInflatedId);
            }
            // 注意:这两步步 ViewSutb 找到自己的位置,并从父View中移除了自己
            // 这会导致 以后调用inflate的时候 再也获取不到 viewParent了
            final int index = parent.indexOfChild(this);
            parent.removeViewInLayout(this);
            // 拿出ViewStub的LayoutParamas,不为null 则会赋值给 StubbedView
            final ViewGroup.LayoutParams layoutParams = getLayoutParams();
            // 把 StubbedView 添加到ViewStub的父View里
            if (layoutParams != null) {
                parent.addView(view, index, layoutParams);
            } else {
                parent.addView(view, index);
            }
            //使用一个弱引用来保存StubbedView
            mInflatedViewRef = new WeakReference(view);
            //回调listener
            if (mInflateListener != null) {
                mInflateListener.onInflate(this, view);
            }
            // 返回 StubbedView
            return view;
        } else {
            // id无效,则throw一个 IllegalArgumentException
            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
        }
    } else {
        // inflate被调用一次后 就没有了ViewParent,就会报这个错
        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
}

我在每行代码上都加上了详细的注释,主要的操作就是把StubbedView给Inflate出来,然后把它放到自己的位置,代码非常清晰,非常简单。

总结来说,其实inflate方法是做了一个『偷梁换柱』的操作,把 StubbedView动态的添加到自己原来的位置上,也因此实现了懒加载功能。

这里还需要注意的是 ViewStub 必须要有一个 Parent,即必须要有父视图!(谢谢 JangGwa 的提醒)

另外值得一提的是:ViewStub还重写了View的setVisibility方法,让我们来分析一下:

public void setVisibility(int visibility) {
    // mInflatedViewRef 保存了 StubbedView还记得吗? inflate过后它就不是null了 
    if (mInflatedViewRef != null) {
        View view = mInflatedViewRef.get();
        // 操作 StubbedView
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        // 操作ViewStub自己,构造方法里的GONE记得么?
        super.setVisibility(visibility);
        // 如果是 VISIBLE INVISIBLE 则会去调用 inflate方法!!!!
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();//注意这一行代码
        }
    }
}

可以看到setVisibility方法中也可能会调用inflate()方法,所以当我们想让StubbedView被加载进来,而我们不需要StubbedView的实例的时候,可以用setVisibility(View.VISIBLE)

不过需要注意的是 不要再接着调用inflate方法,因为此时的 ViewStub 已经被移除了!

要点

  1. 使用ViewStub,必须指定layoutResourceId(必须是布局文件)
  2. 在XML里配置ViewStub的可见性是没有用的
  3. ViewStub 主要原理藏在inflate()方法中,是它把真正要加载的View给加载了进来
  4. inflate()方法只能调用一次
  5. ViewStub调用inflate()后就不要再用它了(让它功成身退!)
  6. 要小心setVisibility方法,因为它可能会调用inflate()
  7. 在XML里给ViewStub设置的LayoutParamas(宽高margin等)会传递给StubbedView,所以我们如果要控制StubbedView的LayoutParamas,则需要写在ViewStub里而不是StubbedView!
  8. 期待补充!

小结

源码分析完毕,可以看到,ViewStub的源码还是非常简单的。

总结来说,它为需要被懒加载的View在布局中占了一个坑,当需要加载时把自己占的坑让给了被加载的 View ,从而实现了懒加载。

推荐阅读

一步一步深入理解CoordinatorLayout
LayoutInflater源码分析(一)之inflate深度分析
LayoutInflater源码分析(二)之include以及merge标签的处理