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,否则会抛异常。嗯,要记住这一点。
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 会直接添加到父布局中。
android:layout_gravity 或 android: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 能省不少布局优化的功夫。但如果是老项目迁移,那就要权衡一下了。
@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,结果性能反而更差了。嗯,工具说话,别靠感觉。