布局优化就会提到ViewStub,提到ViewStub印象里就是按需加载的概念,那ViewStub是怎样实现按需加载的呢?使用ViewStub和setVisibility可以实现相同的效果,那两者有什么区别呢?为什么使用ViewStub可以对布局进行优化?
在布局文件中声明ViewStub:
<ViewStub android:id="@+id/viewStub" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/view"/>
在代码中使用,显示view
ViewStub view = findViewById(R.id.viewStub);
方法一:通过调用setVisibility()方法来
view.setVisibility(View.VISIBLE);
方法二:通过调用inflate()方法,可以返回ViewStub显示的view
View inflateView = view.inflate();
ViewStub继承View,部分源码如下:
@RemoteView
public final class ViewStub extends View {
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);//设置GONE
setWillNotDraw(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
@Override
protected void dispatchDraw(Canvas canvas) {
}
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
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();
}
}
}
}
可以看到在ViewStub构造方法中,调用setVisibility(GONE),设置为GONE;ViewStub的draw()和dispatchDraw()方法都是空方法,没有对子视图进行绘制;在onMeasure()方法中调用了setMeasuredDimension(0, 0)对所有子视图都设置为宽高为0。从这里就可以明白ViewStub 是一个不可见的,大小为0的视图。
ViewStub中复写了setVisibility()方法,在方法里调用inflate()方法。所以通过setVisibility(View.VISIBLE)显示ViewStub,最终会调用inflate()方法,只要弄明白inflate()方法就可以知道ViewStub通过什么方式来显示View的。
ViewStub的inflate()方法源码如下:
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final View view = inflateViewNoAdd(parent);
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");
}
}
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);
}
}
可以看出inflate()方法调用了inflateViewNoAdd()这个方法,这个方法最终通过调用LayoutInflater.from(xx).inflate(xx)方法把对应按需加载的View初始化,再通过调用replaceSelfWithView()方法,先把ViewStub从父布局中移除,再通过addView()将View布局添加进去从而显示View。
看到这里,对于文章开头的几个问题就都弄明白了,那么另一个问题,在使用时注意inflate()方法只能执行一次,所以ViewStub不适合重复显示隐藏的功能,那为什么呢?
mInflatedViewRef通过弱引用形式,建立ViewStub与加载的View的联系,在setVisibility()方法中mInflatedViewRef是null,那么这时候就会走inflate(),在inflate() 方法中给view创建一个WeakReference弱引用,初始化mInflatedViewRef。当第二次调用setVisibility() 的时候,mInflatedViewRef不是null,就会调用 WeakReference 的父类Reference 中的get() 方法获取到view,然后再走View类的setVisibility()方法。
涉及到View怎样从Activity中加载显示问题,不再深入说明。在代码中调用setContentView(R.layout.activity_main)方法最终会调用LayoutInflater.inflate()方法填充布局,源码如下:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在LayoutInflater的inflate()方法会根据参数中的布局资源调用XmlResourceParser实现xml文件解析从而得到一个个的View,而这时候如果你把不需要马上显示的View设置成GONE一样会被解析一样会被加载到内存中去,当然你放到ViewStub中去那么就只会加载ViewStub并不会把相对应的View也加载进去,从而起到懒加载的效果,最终达到布局优化的目的。
参考文档:
https://www.jianshu.com/p/175096cd89ac
https://juejin.im/entry/59b8eaf26fb9a00a52064be7