性能优化之布局优化:ViewStub、Merge、include、过度绘制检测
布局优化这件事,说实话,是Android性能优化里性价比最高的环节之一。你想想看,一个复杂的界面,如果布局层级深、嵌套多,用户滑动时掉帧是必然的。我早期做项目时,就吃过这个亏——一个列表项嵌套了七八层,结果在低端机上卡得不行。后来痛定思痛,把布局优化作为每次代码Review的必查项。
今天咱们就聊聊四个核心手段:ViewStub、Merge、include,以及过度绘制检测。这些工具用好了,你的界面性能会有质的提升。
一、ViewStub:按需加载,别让界面一启动就全盘托出
ViewStub 是个很有意思的控件。它本身不占布局空间,也不参与测量和绘制。只有当你调用 inflate() 或 setVisibility(View.VISIBLE) 时,它才会真正加载指定的布局。
我个人习惯把那些「不是每次都需要显示」的视图用 ViewStub 包起来。比如网络错误提示、加载中的动画、用户协议弹窗,还有广告位——这些玩意儿,用户不一定每次都会看到。
核心优势:ViewStub 在初始化时几乎零开销,只有真正需要时才加载布局。这比用 View.GONE 要高效得多——因为 GONE 虽然不显示,但布局对象已经创建了。
来看一个实际例子:
<!-- 主布局中定义 ViewStub -->
<ViewStub
android:id="@+id/stub_network_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/layout_network_error" />
<!-- 在代码中按需加载 -->
ViewStub stub = findViewById(R.id.stub_network_error);
if (stub != null) {
View errorView = stub.inflate();
// 或者 stub.setVisibility(View.VISIBLE);
}
小技巧:ViewStub 一旦 inflate 之后,它自己就从视图树中移除了,取而代之的是你指定的布局。所以 inflate 之后,原来的 ViewStub 引用就失效了,需要用 inflate 返回的 View 对象来操作。
我在项目中遇到过一个问题:某个页面有七八个不同的状态提示,一开始全用 View.GONE 藏着。结果在低端机上,页面启动时明显卡顿。后来改成 ViewStub 按需加载,启动速度提升了将近 30%。
二、Merge:干掉多余的布局层级
Merge 标签的作用很简单——它可以把子布局直接「合并」到父布局中,不产生额外的 ViewGroup 节点。
你想想看,如果你用 include 引入一个布局,而这个布局最外层是一个 LinearLayout,那就会多出一层嵌套。用 Merge 就能避免这个问题。
<!-- 使用 Merge 的布局文件 merge_layout.xml -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按钮1" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按钮2" />
</merge>
<!-- 在父布局中 include -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<include layout="@layout/merge_layout" />
</LinearLayout>
这样,两个 Button 会直接成为 LinearLayout 的子 View,中间没有多余的节点。
注意:Merge 只能作为根标签使用,而且它必须配合一个合适的父容器。如果父容器是 LinearLayout,那 Merge 里的子 View 就会按 LinearLayout 的规则排列。如果父容器是 RelativeLayout,那就按 RelativeLayout 的规则来。搞错了布局方向,界面会乱掉。
我曾经在一个列表项的布局里,发现嵌套了四层 LinearLayout。用 Hierarchy Viewer 一看,光布局测量就占了 8ms。后来用 Merge 优化了两层,测量时间降到了 3ms 左右。嗯,这个优化效果立竿见影。
三、include:布局复用,别重复造轮子
include 是最基础的布局复用手段。把公共的布局抽出来,比如页面顶部的标题栏、底部的操作栏、加载中的动画,然后在多个页面里引用。
<!-- 公共标题栏 layout_title_bar.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/colorPrimary"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="标题"
android:textColor="#fff"
android:textSize="18sp" />
<ImageView
android:id="@+id/iv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_back" />
</LinearLayout>
<!-- 在页面中引用 -->
<include
layout="@layout/layout_title_bar"
android:layout_width="match_parent"
android:layout_height="48dp" />
include 有个小坑:如果你在 include 标签上设置了 android:layout_* 属性,那被引用的布局的根 View 必须支持这些属性。说白了,就是父布局的 LayoutParams 要能生效。
个人建议:include 配合 Merge 使用效果更佳。如果被引用的布局根节点不需要额外的 ViewGroup,就用 Merge 替代,减少一层嵌套。
四、过度绘制检测:用数据说话
过度绘制(Overdraw)是指屏幕上同一个像素点被多次绘制。说白了,就是 GPU 做了无用功。Android 提供了非常方便的检测工具——在开发者选项里打开「调试 GPU 过度绘制」。
打开之后,屏幕会变成这样:
| 颜色 | 含义 | 建议 |
|---|---|---|
| 无色 | 没有过度绘制 | 完美 |
| 蓝色 | 过度绘制 1 次 | 可接受 |
| 绿色 | 过度绘制 2 次 | 需要关注 |
| 粉色 | 过度绘制 3 次 | 需要优化 |
| 红色 | 过度绘制 4 次及以上 | 必须优化 |
我一般会定个规矩:主界面不允许出现粉色和红色区域。列表页、详情页这些高频页面,绿色区域也要尽量少。
常见的过度绘制原因和解决办法:
- 背景重复设置:Activity 设置了背景,布局又设置了背景,子 View 又设置了背景。其实只需要在最外层设置一次就够了。
- 不必要的背景:有些 View 根本不需要背景色,比如纯文本的 TextView。去掉背景能减少一次绘制。
- 层级过深:嵌套的 ViewGroup 越多,过度绘制越严重。用 Merge 和扁平化布局来解决。
- 自定义 View 的 onDraw 里画了全屏矩形:如果只需要画部分区域,用
clipRect()裁剪。
一个实用的检测流程:
- 打开开发者选项 → 调试 GPU 过度绘制 → 显示过度绘制区域
- 遍历所有页面,截图记录红色和粉色区域
- 针对每个问题区域,分析是哪个 View 导致的
- 用 Layout Inspector 查看布局层级,找到多余的背景或嵌套
- 优化后再次检测,确认问题消失
我记得有一次,一个同事写的页面全是红色。我一看,好家伙——Activity 的背景设了白色,根布局又设了白色,里面的 CardView 又设了白色,CardView 里的 TextView 还设了白色背景。四层白色叠加,GPU 白忙活了。去掉三层冗余背景后,页面流畅度明显提升。
五、综合建议:把这些工具串起来用
布局优化不是孤立地使用某一个工具,而是组合拳:
- 用 include 复用公共布局,减少代码量
- 用 Merge 消除 include 带来的多余层级
- 用 ViewStub 延迟加载非关键视图
- 用 过度绘制检测 验证优化效果
我个人习惯在项目里建一个 layout_common 包,专门放公共布局。然后每个页面开发完成后,必须用过度绘制检测跑一遍。不合格的,打回重做。
说白了,布局优化这件事,前期多花十分钟,后期能省用户十秒钟的等待时间。这笔账,怎么算都划算。
公众号:蓝海资料掘金营,微信deep3321