【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
相关推荐
Kapaseker1 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴2 小时前
Android17 为什么重写 MessageQueue
android
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android