Mark Xu 的博客

记录精彩的程序人生

ViewStub 源码分析

写项目的过程中发现,在需要根据适当条件进行相应 UI 展示时,代码中充斥了 setVisibility 相关的代码,相当混乱。我们可以使用 ViewStub 来简化相应的逻辑,并且 ViewStub 大小为 0,运行时才进行懒加载,所以性能上也有一定优势。本文对 ViewStub 的源码进行分析,以使用的更得心应手!

简单使用

ViewStub 的使用很简单,在布局文件中像引入其他控件一样引入 ViewStub:

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

需要展示 ViewStub 所引用的视图时,通过 id 获取到 ViewStub:

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

除了调用 inflate 方法,还可以调用 setVisible 方法来展示 ViewStub 所引用的 View。下面,我们来看下 ViewStub 的源码。

为统一理解,下面使用 延迟加载 View 来指代 ViewStub 所引用的 View。

源码分析

ViewStub 这个类的代码加上注释也才三百多行,算是很简单的源码类了。先看下成员变量:

/**
 * 延迟加载 View 的 id
 */
private int mInflatedId;
/**
 * 延迟加载 View 的布局资源
 */
private int mLayoutResource;
/**
 * 延迟加载 View 对象的弱引用
 */
private WeakReference<View> mInflatedViewRef;

/**
 * 布局加载器
 */
private LayoutInflater mInflater;
/**
 * 延迟加载 View 加载成功回调接口
 */
private OnInflateListener mInflateListener;

可以通过 ViewStub 的 setOnInflateListener(OnInflateListener inflateListener) 方法设定监听器,在视图加载成功后执行相应的操作,其中加载成功回调接口如下:

public static interface OnInflateListener {
    /**
     * @param stub 把加载延迟加载 View 的那个 ViewStub
     * @param inflated 加载出来的那个延迟加载 View
     */
    void onInflate(ViewStub stub, View inflated);
}

inflate 方法

通常调用 inflate 方法来延迟加载视图,来看看其源码:

public View inflate() {
    final ViewParent viewParent = getParent();

    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            // 延迟加载 View inflate 出来
            final View view = inflateViewNoAdd(parent);
            // 把 ViewStub 自身从父布局视图树中删除,延迟加载 View 加入父布局视图树中
            replaceSelfWithView(view, parent);

            mInflatedViewRef = new WeakReference<>(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,然后根据父布局的约束将延迟加载 View inflate 出来,接着把 ViewStub 自身从父布局视图树中删除,延迟加载 View 加入父布局视图树中。

延迟加载 View 加载成功后,通过弱引用对象保存该视图对象,并回调加载成功回调接口。至此,inflate 方法的工作也就完成了。

其中 inflateViewNoAdd 和 replaceSelfWithView 方法的源码如下,都是很基础的代码:

private View inflateViewNoAdd(ViewGroup parent) {
    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);
    }
    return view;
}

private void replaceSelfWithView(View view, ViewGroup parent) {
    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);
    }
}

setVisibility 方法

通过 ViewStub 的 inflate 方法,我们可以顺利的把想要延迟加载的 View 加载出来了,然而实际的业务逻辑并不会这么简单,有时可能加载出来后需要再次隐藏,隐藏后还要再次加载。或者当 inflate 方法多次调用时,就会报错了:

java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent

通过刚才的源码可以看到,在调用 inflate 方法时,viewParent 为 null 了,为什么呢?原来我们在第一次调用 inflate 方法时,执行到 replaceSelfWithView 方法时,就把 ViewStub 从父布局中移除,然后把延迟加载 View 加入到父布局中了。所以现在再次调用 ViewStub 的 getParent 方法当然为 null 了。

那么延迟加载 View 是否可以多次显示、隐藏呢,当然是可以的,接下来我们看看 ViewStub 的 setVisibility 方法。

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

逻辑很简单:首先判断延迟加载 View 的弱引用对象是否为空,不为空直接获取该 View,并设置相应的可见性;弱引用对象为空的话,如果可见性设置为 VISIBLE 或 INVISIBLE 时,直接调用 inflate 方法进行加载。

需要注意两点:

  • 代码中不调用 inflate 方法,直接调用 setVisibility(View.INVISIBLE) 时,会将延迟加载 View 加载显示出来的
  • ViewStub 在调用一次 inflate 方法后,延迟加载 View 的弱引用对象就不为空了,除非被垃圾回收器扫描到进行回收了。

ViewStub 与 ListView 结合使用的问题

在 ListView 的 item 中使用 ViewStub 根据条件动态加载视图,遇到视图混乱问题,ListView 代码片段如下:

public View getView(int position, View convertView, ViewGroup parent) {
	/*
	if (convertView == null) {
	    ...
	} else {
	    ...
	}
	...
	*/
	holder.mViewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
	    @Override
	    public void onInflate(ViewStub stub, View inflated) {
	        inflated.findViewById(R.id.tv_unapply)
	                .setOnClickListener(new View.OnClickListener() {
	                    @Override
	                    public void onClick(View v) {
	                        // unapply(mEntity.getJobId);
	                    }
	                });
	        inflated.findViewById(R.id.tv_contact_company)
	                .setOnClickListener(new View.OnClickListener() {
	                    @Override
	                    public void onClick(View v) {
	                        // contactCompany();
	                    }
	                });
	    }
	});
	holder.mApplyViewStub.setVisibility(View.VISIBLE);
}

上面源码分析时可知,ViewStub 的 setVisibility 方法在 mInflatedViewRef 不为空时,直接对延迟加载 View 进行可见性设定,并没有执行 inflate 方法,所以我们上面设置的 onInflateListener 就没有回调到了。

所以我们在 ViewHolder 中将 ViewStub 进行缓存后,getView 方法中再次取出时就不会调用 inflate 方法了,所以会出现 onInflateListener 方法回调异常。

总结

ViewStub 源码的几个关键成员变量和方法以及介绍完了。可以看到,ViewStub 的延迟加载特性,在提升视图性能的同时,还可以使业务逻辑更清晰,大量减少 View 的 setVisibility 相关代码。

使用时,只需要一次性加载的可以直接调用 inflate 方法进行加载。需要多次调用修改可见性的,可以调用 ViewStub 的 setVisibility 方法,同时可以通过设置监听器在加载完成后执行相应的操作。

留下你的脚步