【Android】布局优化:include、merge、ViewStub以及Inflate()源码浅析

include

引入:

include 是 Android 布局复用的利器,它让你能够将常用的布局组件封装成独立的 XML 文件,然后在多个地方重复使用;比如应用中的统一标题栏、通用底部按钮等,通过 include 只需定义一次,就能在任何需要的界面中嵌入,极大提高了代码的复用性和维护性;

  1. include是为了实现布局的复用;
  2. 使用:

include_title1文件中写我们需要复用的文件;

java 复制代码
<?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">
    <Button
        android: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写入我们的布局;

java 复制代码
<?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">
    <include
       android:id = "@+id/bt_title"
        android:layout_width="100dp"
        android:layout_height="100dp"
        layout="@layout/include_title">

    </include>
    <include
        android:id = "@+id/bt_titles"
        layout="@layout/include_title1">

    </include>
</androidx.constraintlayout.widget.ConstraintLayout>
  1. 如果需要找到子项,我们需要找到对应的include的布局,然后用布局.findviewbyid查找对应的Id;

  2. 注意事项:

    1. 建议为每个include设置不同的id,因为如果一个布局中重复两个相同的layout或者有重复的Id,此时只会找到第一个被引入的layout中的元素;
    2. 后面修改只能修改布局(除背景颜色等等),而且修改布局的前提也必须重新定义宽高
    3. 如果layout的根布局设置了id,include里也设置了Id,建议保持一致,否则可能返回Null;

merge

引入:

merge 是优化布局层级的精妙工具,它可以自动消除多余的ViewGroup层级。当使用 include 引入布局时,如果外层容器与父布局类型相同,merge 会直接将其中的子视图融合到父容器中,避免产生不必要的嵌套,从而简化视图结构、提升测量和绘制效率,让布局更加高效;

  1. 为了减少视图层级以优化布局,提升UI渲染的性能;

  2. 使用:

    其实很简单,就直接把根布局用Merge替换了就行;

    java 复制代码
    <?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">
        <Button
            app:layout_constraintTop_toTopOf="parent"
            android:id = "@+id/bt_title"
    
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="dsf">
    
        </Button>
    </merge>
  3. 其他:什么时候用Merge,就是当前layout的父容器和layout的根容器相同的类型,那么layout的根容器可以使用merge 来代替,减少布局嵌套,增加UI渲染效果;

  4. 注意事项:

    1. 因为Activity的默认contentview外层就是一个framelayout,所以当前布局如果是framelayout而且无背景,边距其他属性的设置,就可以用merge来代替;
    2. 等等。。。为什么还有边距?这是因为使用Merge没有根布局,所以设置边距或者宽高默认无效;
    3. merge的使用范围:必须作为根结点使用,否则就会报错;
    4. 在使用inflate时候,必须指定父容器,而且第三个参数设置为true立刻加入到父视图中;
    5. 因为merge无法独立生成视图对象,但是viewStub是动态生成视图,所以viewstub中禁止使用merge

ViewStub

  1. 使用情况: 页面初始化时,可能会隐藏一些不可见的veiw ,就算隐藏了,但是在初始化时还是会创建,为了减少开销,有一个轻量级的方案--viewstub

  2. 使用:

    写一个布局是自己想加载的布局然后在总布局布局中使用viewstub控件,但是必须设置宽高,并且指定layout,最后的宽高也就是这个viewstub内所定义的宽高,所以一定要保持一致,否则就有可能出现布局偏差;

    java 复制代码
    <ViewStub
        android: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>
  3. 注意事项:

  4. (布局嵌套问题)Viewstub是一个动态加载的布局,所以根布局不可以使用merge,所以它可能会存在一些布局嵌套;

  5. (一次inflate)初始化使用--viewstub懒加载机制决定在第一次inflate之后或者设置可见性,就不能再inflate了,因为inflate会把view从自身替换到布局文件中的目标视图中,并从布局树中移除;

    设置可见性是因为,第一次设置的时候会执行inflate的过程,后面设置的时候,就是对已经在目标视图中的view进行可见性的设置,不会inflate了;

    如果想调用多次呢,保存inflate的引用,设置可见性;

  6. (目标布局的占位符--宽高) viewstuB自身不参与绘制占用空间,但是它仍然是一个有效的占位符,所以布局文件中需要写宽高,也是最终显示出来的宽高;

源码分析:

主要解析都写在注释了,大家自行看

inflate()

java 复制代码
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");
    }
}
  1. 这里保存的是弱引用,但是内存紧张的时候可能会被GC机制回收,所以我们应该自己保存强引用;

inflateViewNoAdd()

java 复制代码
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()

java 复制代码
private void replaceSelfWithView(View view, ViewGroup parent) {
    //  获得当前viewstub在父容器中的位置
    final int index = parent.indexOfChild(this);
    //  移除这个viewstub
    parent.removeViewInLayout(this);
    //  获得这个viewstub的布局参数
    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    //  添加新的视图
    if (layoutParams != null) {
        parent.addView(view, index, layoutParams);
    } else {
    //使用默认的布局参数
        parent.addView(view, index);
    }
}
  1. 调用一次inflate之后viewstub就会从布局树中被移除,所以这里inflate只能调用一次;

过程就是:得到位置,删除viewstub, 最后添加新的视图;

setVisbility()

java 复制代码
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(); 
        }
    }
}
  1. 设置为GONE时,不会触发Inflate,这是为了性能考虑;所以不能通过设置GONE来触发Inflate;

源码总结:inflate的过程其实是判断条件(如果viewstub添加到布局树中而且是有效的布局资源)->通过布局加载器加载这个布局得到view(但是不加入到父布局中,因为你需要加到原来的位置)->得到原来的位置,删除这个viewstub,并且把这个布局加载进去,最后保存弱引用触发回调;

知道了源码剩下的就是我们需要注意的点的总结:

  1. 在创建新view时,我们需要注意我们需要设置InflatedId,这样我们才能得到新布局;
  2. 需要自己保存强引用,否则会有异常;
  3. 因为会移除,所以只能调用一次;
  4. 设置可见性,第一次不能设置为gone,否则不会触发inflate
相关推荐
GISer_Jing2 小时前
2025年Flutter与React Native对比
android·flutter·react native
MasterLi80233 小时前
我的读书清单
android·linux·学习
怪兽20143 小时前
fastjson在kotlin不使用kotlin-reflect库怎么使用?
android·开发语言·kotlin
彭同学学习日志3 小时前
Kotlin Fragment 按钮跳转报错解决:Unresolved reference ‘floatingActionButton‘
android·开发语言·kotlin
Gracker5 小时前
Android Perfetto 系列 9 - CPU 信息解读
android
Gracker5 小时前
Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
android
Gracker5 小时前
Android Perfetto 系列 07 - MainThread 和 RenderThread 解读
android
Gracker5 小时前
Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
android
Gracker5 小时前
Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
android