【Android】性能优化——懒加载控件ViewStub简易使用与源码分析
前言
为什么ViewStub可以提高加载性能?
ViewStub使用的是惰性加载的方式,即使将其放置于布局文件中,如果没有进行加载那就为空,不像其它控件一样只要布局文件中声明就会存在。
那ViewStub适用于场景呢?通常用于网络请求页面失败的显示。一般情况下若要实现一个网络请求失败的页面,我们是不是使用两个View呢,一个隐藏,一个显示。试想一下,如果网络状况良好,并不需要加载失败页面,但是此页面确确实实已经加载完了,无非只是隐藏看不见而已。如果使用ViewStub,在需要的时候才进行加载,不就达到节约内存提高性能的目的了吗?
正文
MainActivity
public class MainActivity extends AppCompatActivity {
Button mShowButton, mHideButton;
ViewStub mViewStub;
LinearLayout mFillView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewStub = findViewById(R.id.vs);
mShowButton = findViewById(R.id.btn_show_stub);
mHideButton = findViewById(R.id.btn_hide_stub);
mShowButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
//inflate只能执行一次,多次会报错
//获得替换view的两种方式
//1.通过inflate返回值
mFillView = (LinearLayout) mViewStub.inflate();
//2.通过在xml中设置ViewStub的inflatedId属性也可获得替换view
//fillView = findViewById(R.id.inflated_start);
} catch (Exception e) {
//加载过后显隐使用visible
//有2种方式控制显隐效果
//1.使用原本的viewStub
//mViewStub.setVisibility(View.VISIBLE); 使用mViewStub也可以控制内容显隐
//2.直接使用替换内容view
mFillView.setVisibility(View.VISIBLE);
}
}
});
mHideButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//mViewStub.setVisibility(View.GONE); 使用viewStub也可以控制内容显隐
mFillView.setVisibility(View.GONE);
}
});
}
}
ps:加载也可以使用 viewStub.setVisibility(),内部源码还是inflate();
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ViewStub
android:id="@+id/vs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inflatedId="@+id/inflated_start"
android:layout="@layout/view_stub"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="展示stub"
android:id="@+id/btn_show_stub"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="隐藏stub"
android:id="@+id/btn_hide_stub"/>
</LinearLayout>
替换View
view_stub.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="我是来替它的" />
</LinearLayout>
预览
效果图
layout inspector分析
1.在没点击“展示”时 是灰色的ViewStub
2.点击展示后被替换成自己的控件
3.再点击隐藏,灰掉的是替换后的控件
结论
ViewStub的使用是将自己的位置替换成它的内容view,不保留自己,所以替换过后,布局中将无法找到原来的ViewStub。
问题
在这次demo使用ViewStub的过程中,我希望大家能有几个问题?
1.为什么ViewStub可以提高性能?
2.visible:gone不是也没有渲染view,为什么还需要ViewStub?
3.在替换ViewStub占用位置为内容替换view的时候是如何准确替换位置的? (这是我的疑惑...)
4.为什么我在替换内容中使用的是满屏layout,加载出来只有那么一丢丢?
5.为什么ViewStub是去除自己,替换内容,却还能使用已经卸载掉的ViewStub控制内容显隐?
其上1.2两个问题都在以下的借鉴博客中找到,希望大家了解
那咱们就从源码角度分析一下3,4两个问题
最新版源码
起点 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);
//替换ViewStub过程 笔者自加
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);
}
//第三个参数传入false即代表不添加到parent
final View view = factory.inflate(mLayoutResource, parent, false);
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
return view;
}
咱们就来看一下关键代码,替换
private void replaceSelfWithView(View view, ViewGroup parent) {
//使用index获得子控件(ViewStub)索引
final int index = parent.indexOfChild(this); //之前从来没用过indexOfChild的我,面壁
parent.removeViewInLayout(this);
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
//使用index索引替换到指定位置
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
解析
- 使用parent.indexOfChild可以获得子view在父view 的索引位置index
- 使用index定向addView( view , index , layoutParams) 装载替换view
这就是为什么能定点替换了。
第四问,为什么不按替换view样式显示呢?
- 我们设置了layoutParams,于是使用的是addView( view , index , layoutParams) 方法
- 跟随源码一路发现
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
//不传就是使用子控件自己的,不过可想而知很少会出现不传的这个addView
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
public void addView(View child, int index, LayoutParams params) {
...省略
addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
...省略
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
if (preventRequestLayout) {
//被设置成传入的
child.mLayoutParams = params;
} else {
child.setLayoutParams(params);
}
...省略
}
解析
-
replaceSelfWithView()中的layoutParams大概率都是存在的,于是会调用addView(View child, int index, LayoutParams params)
- 使用传入的params,也就是我们写在ViewStub中的wrap_content
第五问,为什么ViewStub是去除自己,替换内容,却还能使用已经卸载掉的ViewStub控制内容显隐?
答:虽然ViewStub从view Tree中卸载,但是还处在内存中。所以ViewStub可以通过使用WeakReference弱引用持有内容view,再进行显隐,其中的setVisibility()源码就有使用到。
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
View view = mInflatedViewRef.get();//通过inflate()加载过的weakReference
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();
}
}
}
借鉴
上一篇: python 画图总结