【Android】布局优化:include、merge、ViewStub以及Inflate()源码浅析
include
引入:
include是 Android 布局复用的利器,它让你能够将常用的布局组件封装成独立的 XML 文件,然后在多个地方重复使用;比如应用中的统一标题栏、通用底部按钮等,通过include只需定义一次,就能在任何需要的界面中嵌入,极大提高了代码的复用性和维护性;
- include是为了实现布局的复用;
- 使用:
在include_title1文件中写我们需要复用的文件;
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"xmlns:app="http://schemas.android.com/apk/res-auto"><Buttonandroid:layout_marginTop="5dp"android:backgroundTint="#000000"app:layout_constraintTop_toTopOf="parent"android:id = "@+id/bt_titles"app:layout_constraintBottom_toBottomOf="parent"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="dianji"></Button></androidx.constraintlayout.widget.ConstraintLayout>
在我们正式的布局中,通过Include写入我们的布局;
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><includeandroid:id = "@+id/bt_title"android:layout_width="100dp"android:layout_height="100dp"layout="@layout/include_title"></include><includeandroid:id = "@+id/bt_titles"layout="@layout/include_title1"></include>
</androidx.constraintlayout.widget.ConstraintLayout>
-
如果需要找到子项,我们需要找到对应的include的布局,然后用布局.
findviewbyid查找对应的Id; -
注意事项:
- 建议为每个include设置不同的id,因为如果一个布局中重复两个相同的layout或者有重复的Id,此时只会找到第一个被引入的layout中的元素;
- 后面修改只能修改布局(除背景颜色等等),而且修改布局的前提也必须重新定义宽高
- 如果layout的根布局设置了id,include里也设置了Id,建议保持一致,否则可能返回Null;
merge
引入:
merge是优化布局层级的精妙工具,它可以自动消除多余的ViewGroup层级。当使用include引入布局时,如果外层容器与父布局类型相同,merge会直接将其中的子视图融合到父容器中,避免产生不必要的嵌套,从而简化视图结构、提升测量和绘制效率,让布局更加高效;
-
为了减少视图层级以优化布局,提升UI渲染的性能;
-
使用:
其实很简单,就直接把根布局用Merge替换了就行;<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"xmlns:app="http://schemas.android.com/apk/res-auto"><Buttonapp:layout_constraintTop_toTopOf="parent"android:id = "@+id/bt_title"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="dsf"></Button> </merge> -
其他:什么时候用
Merge,就是当前layout的父容器和layout的根容器相同的类型,那么layout的根容器可以使用merge来代替,减少布局嵌套,增加UI渲染效果; -
注意事项:
- 因为Activity的默认
contentview外层就是一个framelayout,所以当前布局如果是framelayout而且无背景,边距其他属性的设置,就可以用merge来代替; - 等等。。。为什么还有边距?这是因为使用
Merge没有根布局,所以设置边距或者宽高默认无效; merge的使用范围:必须作为根结点使用,否则就会报错;- 在使用
inflate时候,必须指定父容器,而且第三个参数设置为true立刻加入到父视图中; - 因为
merge无法独立生成视图对象,但是viewStub是动态生成视图,所以viewstub中禁止使用merge;
- 因为Activity的默认
ViewStub
-
使用情况: 页面初始化时,可能会隐藏一些不可见的
veiw,就算隐藏了,但是在初始化时还是会创建,为了减少开销,有一个轻量级的方案--viewstub -
使用:
写一个布局是自己想加载的布局然后在总布局布局中使用
viewstub控件,但是必须设置宽高,并且指定layout,最后的宽高也就是这个viewstub内所定义的宽高,所以一定要保持一致,否则就有可能出现布局偏差;<ViewStubandroid:id = "@+id/ljx"android:layout="@layout/viewstub_01"android:layout_width="200dp"android:layout_height="100dp"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toTopOf="@+id/bt_titles"app:layout_constraintLeft_toLeftOf="parent"></ViewStub> -
注意事项:
-
(布局嵌套问题)
Viewstub是一个动态加载的布局,所以根布局不可以使用merge,所以它可能会存在一些布局嵌套; -
(一次
inflate)初始化使用–viewstub懒加载机制决定在第一次inflate之后或者设置可见性,就不能再inflate了,因为inflate会把view从自身替换到布局文件中的目标视图中,并从布局树中移除;设置可见性是因为,第一次设置的时候会执行
inflate的过程,后面设置的时候,就是对已经在目标视图中的view进行可见性的设置,不会inflate了;如果想调用多次呢,保存
inflate的引用,设置可见性; -
(目标布局的占位符–宽高)
viewstuB自身不参与绘制占用空间,但是它仍然是一个有效的占位符,所以布局文件中需要写宽高,也是最终显示出来的宽高;
源码分析:
主要解析都写在注释了,大家自行看
inflate()
public View inflate() {final ViewParent viewParent = getParent();//前置条件检查,当前viewstub必须添加到父容器,才能inflate;if (viewParent != null && viewParent instanceof ViewGroup) {//而且必须设置有效的布局资源;if (mLayoutResource != 0) {final ViewGroup parent = (ViewGroup) viewParent;// 创建替换到viewstub位置的view;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");}
}
- 这里保存的是弱引用,但是内存紧张的时候可能会被GC机制回收,所以我们应该自己保存强引用;
inflateViewNoAdd()
private View inflateViewNoAdd(ViewGroup parent) {final LayoutInflater factory;//获得layoutInfalter的实例if (mInflater != null) {//viewstub可以设置了自定义的布局加载器factory = mInflater;} else {//使用默认的布局加载器factory = LayoutInflater.from(mContext);}// 把我们的布局资源,方到未来父容器中(不是立即添加到父容器)final View view = factory.inflate(mLayoutResource, parent, false);// 这个就是替换后的view的Id;if (mInflatedId != NO_ID) {view.setId(mInflatedId);}return view;
}
这个方法的目的就是创建view对象,解析布局参数,设置视图的属性;
主要过程就是得到布局加载器,然后设置视图,最后设置Id;
replaceSelfWithView()
private void replaceSelfWithView(View view, ViewGroup parent) {// 获得当前viewstub在父容器中的位置final int index = parent.indexOfChild(this);// 移除这个viewstubparent.removeViewInLayout(this);// 获得这个viewstub的布局参数final ViewGroup.LayoutParams layoutParams = getLayoutParams();// 添加新的视图if (layoutParams != null) {parent.addView(view, index, layoutParams);} else {//使用默认的布局参数parent.addView(view, index);}
}
- 调用一次
inflate之后viewstub就会从布局树中被移除,所以这里inflate只能调用一次;
过程就是:得到位置,删除viewstub, 最后添加新的视图;
setVisbility()
public void setVisibility(int visibility) {if (mInflatedViewRef != null) {//如果inflate过,通过弱引用从取出来这个view设置可见性;如果被gc机制回收,那么会爆出异常,所以简易我们使用强引用;View view = mInflatedViewRef.get();if (view != null) {view.setVisibility(visibility);} else {throw new IllegalStateException("setVisibility called on un-referenced view");}} else {//没有Inflate过的话,调用;super.setVisibility(visibility);if (visibility == VISIBLE || visibility == INVISIBLE) {inflate(); }}
}
- 设置为
GONE时,不会触发Inflate,这是为了性能考虑;所以不能通过设置GONE来触发Inflate;
源码总结:inflate的过程其实是判断条件(如果viewstub添加到布局树中而且是有效的布局资源)->通过布局加载器加载这个布局得到view(但是不加入到父布局中,因为你需要加到原来的位置)->得到原来的位置,删除这个viewstub,并且把这个布局加载进去,最后保存弱引用触发回调;
知道了源码剩下的就是我们需要注意的点的总结:
- 在创建新
view时,我们需要注意我们需要设置InflatedId,这样我们才能得到新布局;- 需要自己保存强引用,否则会有异常;
- 因为会移除,所以只能调用一次;
- 设置可见性,第一次不能设置为
gone,否则不会触发inflate;
