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

2025-12-04 0 207

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

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

一、include

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

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

my_title_layout.xml

<?xml version=\"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>

使用也很简单,如下:

<?xml version=\"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

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

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

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

<?xml version=\"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代码如下:

<?xml version=\"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

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

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

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

布局中使用:

<?xml version=\"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 主要用于按需加载布局,提高初始布局性能,只有在需要时才加载视图。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 开发教程 【Android】布局优化:include、merge、ViewStub的使用及注意事项 https://www.zuozi.net/3324.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务