3、布局优化进阶:ViewStub与Merge标签的使用、自定义ViewGroup优化、异步布局框架(Litho/Compose)简介

布局优化,说白了就是让界面加载更快、渲染更流畅。很多同学觉得布局优化就是少嵌套几层,其实远不止这么简单。今天咱们聊聊几个进阶技巧,都是我实际项目中反复用过的。

3.1 ViewStub:按需加载的利器

先说说 ViewStub。这东西看着不起眼,但用好了能省不少性能。

ViewStub 是个轻量级的 View,它不占布局空间,也不参与绘制。只有当你调用 inflate()setVisibility(View.VISIBLE) 时,它才会加载指定的布局。

我习惯用它来处理那些「不一定出现」的 UI 元素。比如网络错误提示、用户引导浮层、或者某些高级功能面板。

核心用法:
<ViewStub
    android:id="@+id/error_stub"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout="@layout/layout_error" />

// 在代码中按需加载
ViewStub errorStub = findViewById(R.id.error_stub);
if (needShowError) {
    errorStub.inflate();
}

这里有个坑,我曾经踩过:ViewStub 一旦 inflate 之后,它自己就从视图树中消失了,取而代之的是你指定的布局。所以你不能重复 inflate,否则会抛异常。嗯,要记住这一点。

我的建议:用 ViewStub 时,最好配合一个布尔变量记录是否已经加载过。比如:
private boolean isErrorViewInflated = false;

private void showErrorView() {
    if (!isErrorViewInflated) {
        ViewStub stub = findViewById(R.id.error_stub);
        stub.inflate();
        isErrorViewInflated = true;
    }
}

3.2 Merge标签:减少嵌套层级

Merge 标签更简单,但很多人用不对。它的作用就是去掉多余的父布局

举个例子。你写了个自定义的标题栏布局,里面有个 RelativeLayout 作为根节点。然后你在 Activity 的布局里又用 <include> 把它引进来。这时候,include 进来的布局会多一层 RelativeLayout 的嵌套。如果这个 RelativeLayout 没什么实际作用,那就是浪费。

用 Merge 就能解决这个问题:

<!-- title_bar.xml -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
        android:id="@+id/title_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/back_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</merge>

这样 include 进来时,Merge 标签不会生成额外的 ViewGroup,它的子 View 会直接添加到父布局中。

注意:Merge 标签只能作为布局文件的根节点。而且它不支持 android:layout_gravityandroid:layout_ 开头的属性。如果你需要这些属性,那就别用 Merge 了。

3.3 自定义ViewGroup优化

说到自定义 ViewGroup,很多人的第一反应是「重写 onMeasure 和 onLayout」。没错,但优化点往往藏在细节里。

我做过一个流式布局(FlowLayout),用来展示标签列表。一开始直接套用 LinearLayout 嵌套,结果列表一长就卡顿。后来改成自定义 ViewGroup,性能提升很明显。

优化的关键点有几个:

  • 减少 onMeasure 的调用次数:onMeasure 可能会被多次调用,尽量缓存测量结果。比如子 View 的尺寸如果不变,就别重复测量。
  • 避免在 onLayout 中做耗时操作:onLayout 是逐帧调用的,里面别做对象创建、文件读写之类的事。
  • 使用 LayoutParams 传递自定义属性:别在 onMeasure 里解析 TypedArray,那东西开销不小。
一个优化示例:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 先测量子 View,但只测量一次
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
    }
    // 计算自己的尺寸
    setMeasuredDimension(resolveSize(width, widthMeasureSpec),
                         resolveSize(height, heightMeasureSpec));
}

你想想看,如果每次 onMeasure 都重新测量所有子 View,那性能能好吗?

3.4 异步布局框架简介

最后聊聊异步布局。这是个比较新的方向,但效果确实好。

传统的布局是在 UI 线程上执行的。如果布局复杂,UI 线程就会被阻塞,导致掉帧。异步布局的思路是:把布局计算放到后台线程,等计算完了再提交到 UI 线程渲染。

3.4.1 Litho

Litho 是 Facebook 开源的异步布局框架。它用了一种叫「Yoga」的布局引擎,支持 Flexbox 布局。

我记得在项目中用过 Litho 优化一个复杂的 Feed 流。原本的布局嵌套了七八层,滑动时明显卡顿。换成 Litho 后,帧率从 30fps 提升到了 55fps 左右。

不过 Litho 的学习曲线有点陡。你得用它的 @Prop@State 这些注解来声明组件,跟传统的 View 体系差别挺大。

简单示例:
@LayoutSpec
public class MyComponentSpec {
    @OnCreateLayout
    static Component onCreateLayout(
            ComponentContext c,
            @Prop String title) {
        return Text.create(c)
                .text(title)
                .textSizeSp(16)
                .build();
    }
}

3.4.2 Jetpack Compose

Compose 是 Google 官方的声明式 UI 框架。它虽然不完全是「异步布局」,但它的布局机制比传统 View 系统高效很多。

Compose 的布局是基于约束的,而且它用了一种叫「intrinsic measurements」的技术,能减少测量次数。说白了,就是 Compose 知道每个组件的最小尺寸和最大尺寸,不需要反复测量。

我个人觉得,Compose 更适合新项目。如果你是从头开始写一个 App,直接用 Compose 能省不少布局优化的功夫。但如果是老项目迁移,那就要权衡一下了。

Compose 布局示例:
@Composable
fun MyScreen() {
    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Text("标题", fontSize = 20.sp)
        Spacer(modifier = Modifier.height(8.dp))
        Text("内容内容内容...")
    }
}

3.5 总结与建议

布局优化这件事,没有银弹。不同的场景用不同的方案:

场景 推荐方案 原因
按需显示的 UI ViewStub 延迟加载,减少初始布局时间
include 引入的布局 Merge 减少嵌套层级
复杂自定义布局 自定义 ViewGroup 精确控制测量和布局过程
高性能 Feed 流 Litho / Compose 异步布局或更高效的测量机制

最后说一句:别为了优化而优化。先用 Profiler 看看瓶颈在哪,再对症下药。我曾经见过有人把简单的 LinearLayout 改成自定义 ViewGroup,结果性能反而更差了。嗯,工具说话,别靠感觉。