【Android】布局优化:include、merge、ViewStub的使用及注意事项

Java教程 2025-10-23

【Android】布局优化:include、merge、ViewStub的使用及注意事项

在 Android 布局优化中,include、merge 和 ViewStub 是三种常用的布局标签。include 主要用于布局重用,merge 一般和 include 配合使用,它可以减少布局嵌套层级,而 ViewStub 则提供了按需加载的功能,当需要时才会将 ViewStub 中的布局加载到内存,提高了程序初始化效率,下面分别介绍它们的使用方法:

一、include

在 Android 开发中, 标签用于实现布局复用。我们通常会将一些通用的界面元素单独抽取到一个独立的布局文件中,然后通过 标签在其他布局中进行引用。这样不仅方便对相同视图进行统一维护和修改,也有效提高了布局的重用性与开发效率。

举个栗子,以标题栏为例,抽取布局如下:

my_title_layout.xml

"1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#00BCD4">

    <ImageButton
        android:id="@+id/back_btn"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@drawable/ic_back"
        android:backgroundTint="#00FFFFFF"/>

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginStart="20dp"
        android:layout_toEndOf="@+id/back_btn"
        android:gravity="center"
        android:text="我的title"
        android:textSize="18sp" />

RelativeLayout>

使用也很简单,如下:

"1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/my_title_layout"/>

LinearLayout>

注意事项

include 使用有几点需要注意:

  1. 当同一个 XML 布局文件中包含多个 标签时,建议为每个 单独设置 id 属性。否则,在代码中通过 findViewById() 获取子视图时,只能找到第一个被引入的布局及其内部控件,后续的 所对应的视图将无法正确访问。
  2. 如果被引入的布局文件的根视图本身定义了 android:id,而 标签也设置了 android:id,则建议保持两者一致。否则在代码中通过 findViewById() 访问根视图时,可能会出现返回 null 的情况。
  3. 标签中,我们可以重写被引入布局中的所有 layout 属性,但无法重写普通的非 layout 属性(如背景颜色、文字大小等)。需要特别注意的是,若要在 标签中对 layout 属性进行重写,必须同时显式指定 layout_widthlayout_height,否则所覆写的属性将不会生效。

二、merge

merge标签可用于减少视图层级来优化布局,可以配合include使用,如果include标签的父布局 和 include布局的根容器是相同类型的,那么根容器的可以使用merge代替。标签存在着一个不好的地方,可能会导致产生多余的布局嵌套。举个栗子:

my_choice_layout.xml

"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">

    <Button
        android:id="@+id/ok"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:layout_marginStart="40dp"
        android:text="确定"/>

    <Button
        android:id="@+id/cancel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:layout_marginStart="40dp"
        android:text="取消"/>

LinearLayout>

这里定义了两个按钮,在布局中引用:

"1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/my_title_layout"/>

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="输入"
        android:layout_margin="40dp"/>

    <include layout="@layout/my_choice_layout"/>

LinearLayout>

运行结果如下:

看起来没什么问题,其实不知不觉中我们多嵌套了一层布局。我们用工具查看一下此时布局结构:

其实这种情况下:在主界面中,标签的parent ViewGroup与包含的layout根容器 ViewGroup 是相同的类型,这里都是LinearLayout,那么则可以将包含的 layout 根容器 ViewGroup 使用标签代替,从而减少一层 ViewGroup 的嵌套,提升UI渲染性能。

修改my_choice_layout.xml代码如下:

"1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <Button
        android:id="@+id/ok"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:layout_marginStart="40dp"
        android:text="确定"/>

    <Button
        android:id="@+id/cancel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:layout_marginStart="40dp"
        android:text="取消"/>

merge>

此时布局结构如下:

可以看到,这里去除了多余的嵌套。

注意事项

  1. 如果一个布局文件的根容器是 FrameLayout,且没有设置 backgroundpadding 等属性,那么完全可以使用 来替代它。因为 Activity 的默认 ContentView 外层本身就是一个 FrameLayout,此时再嵌套一层 FrameLayout 会造成多余的层级。使用 可以让布局内容直接插入到父容器中,从而减少渲染层次,提升性能。
  2. 由于 并非一个实际的 View 对象,因此在通过 LayoutInflater.inflate() 手动加载时必须为其指定父容器,并且第三个参数要传入 true,表示将子视图立即附加到父容器中。
  3. 只能作为布局文件的根节点使用,不能嵌套在其他布局中。如果它出现在非根层级位置,Android Studio 会直接报错或在运行时崩溃。此外,ViewStub 引用的布局文件中禁止使用 作为根节点,因为 ViewStub 会通过 inflate() 动态创建视图,而 无法独立生成视图对象,这会导致 InflateException 异常。
  4. 与普通布局不同,当使用 引入一个以 为根的布局时,不能在 标签中重写布局属性(如 layout_widthlayout_height),因为 没有自己的根容器,这些属性会被直接忽略。

三、ViewStub

在开发中,我们可能会遇到这样的情况:页面中存在一些在初始化阶段暂时不需要显示的布局。虽然可以通过将它们的可见性设置为 invisiblegone 来隐藏,但这些布局在界面加载时依然会被解析与创建,从而增加页面的初始化开销。为了解决这一问题,Android 提供了一个轻量级的解决方案 —— ViewStub。它是一个不可见、尺寸为 0 的占位视图,具备 懒加载(延迟加载) 的特性。ViewStub 虽然存在于视图层级结构中,但只有在调用 setVisibility()inflate() 方法时才会真正加载并替换成目标布局,因此不会影响页面的初始渲染性能。

举个栗子:

extra_layout.xml

"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">

    <EditText
        android:id="@+id/et_1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:layout_marginStart="40dp"
        android:hint="学号"/>

    <EditText
        android:id="@+id/et_2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:layout_marginStart="40dp"
        android:hint="班级"/>

LinearLayout>

这里设置两个输入框,作为要延迟加载的布局。

布局中使用:

"1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/my_title_layout"/>

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="姓名"
        android:layout_marginTop="40dp"
        android:layout_marginStart="40dp"
        android:layout_marginEnd="40dp"/>
    
    <Button
        android:id="@+id/btn_more"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="更多"
        android:layout_gravity="end"/>
    
    <ViewStub
        android:id="@+id/view_stub"
        android:layout="@layout/extra_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <include layout="@layout/my_choice_layout"/>

LinearLayout>

在代码中加载:

public class MainActivity extends AppCompatActivity {
    
    private EditText editText1;
    private EditText editText2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        Button button = (Button) findViewById(R.id.btn_more);
        button.setOnClickListener(v -> {
            ViewStub viewStub = (ViewStub) findViewById(R.id.view_stub);
            if(viewStub != null) {
                View view = viewStub.inflate();
                editText1 = view.findViewById(R.id.et_1);
                editText2 = view.findViewById(R.id.et_2);
            }
        });
    }
}

运行程序,效果如下:

注意事项

  1. 由于 ViewStub 不是一个实际的视图容器,因此它在加载布局时不支持使用 作为根布局。因此这有可能导致加载出来的布局存在着多余的嵌套结构。
  2. ViewStub 的懒加载机制决定了它在第一次调用 inflate() 或设置 setVisibility(View.VISIBLE) 后会被实际布局替换,并从视图树中移除。因此,同一个 ViewStub 不能被重复加载。如果第二次调用 inflate(),系统会抛出 IllegalStateException 异常。若需多次显示该布局,建议保存 inflate() 返回的视图引用,通过 setVisibility() 控制显示与隐藏。
  3. 虽然 ViewStub 自身不参与绘制,也几乎不占用空间,但它仍然是一个有效的视图占位符。因此,布局文件中若未显式声明 android:layout_widthandroid:layout_height,系统在解析时会抛出异常。

源码分析

inflate() 方法分析
public View inflate() {
    final ViewParent viewParent = getParent();
    
    // 前置条件检查
    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            // 1. 创建视图但不立即添加
            final View view = inflateViewNoAdd(parent);
            // 2. 用新视图替换自身
            replaceSelfWithView(view, parent);
            // 3. 保存弱引用并触发回调
            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");
    }
}

inflateViewNoAdd() 创建视图但不添加:

private View inflateViewNoAdd(ViewGroup parent) {
    final LayoutInflater factory;
    if (mInflater != null) {
        factory = mInflater;
    } else {
        factory = LayoutInflater.from(mContext);
    }
    // inflate但不attach到parent,避免重复添加
    final View view = factory.inflate(mLayoutResource, parent, false);
    // 设置inflated ID
    if (mInflatedId != NO_ID) {
        view.setId(mInflatedId);
    }
    return view;
}

replaceSelfWithView() 替换操作:

private void replaceSelfWithView(View view, ViewGroup parent) {
    // 获取当前ViewStub在父容器中的位置
    final int index = parent.indexOfChild(this);
    // 从父容器中移除ViewStub
    parent.removeViewInLayout(this);
    // 获取ViewStub的LayoutParams
    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    // 将新视图添加到原来ViewStub的位置
    if (layoutParams != null) {
        parent.addView(view, index, layoutParams);
    } else {
        parent.addView(view, index);
    }
}
setVisibility() 方法分析
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        // 情况1:已经inflate过
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        // 情况2:还未inflate
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();  // 触发inflate
        }
    }
}

总结

  • include 主要用于布局复用,将公共的布局部分提取出来,在多个地方重复使用。
  • merge 主要用于减少布局层级,消除不必要的 ViewGroup,优化布局性能。
  • ViewStub 主要用于按需加载布局,提高初始布局性能,只有在需要时才加载视图。