15、Android性能优化之布局优化:减少布局层级、使用ViewStub、Merge标签、ConstraintLayout优化
布局优化,说白了就是让界面渲染得更快、更流畅。我见过不少项目,功能没问题,但一滑动就掉帧,点个按钮要等半天才响应。排查下来,十有八九是布局写得太“重”了。
今天咱们就聊聊布局优化的几个核心手段。这些技巧我在实际项目中反复用过,效果立竿见影。
为什么布局会拖慢性能?
Android的布局渲染,本质上是一个“测量-布局-绘制”的过程。每次界面刷新,系统都要从根布局开始,一层层往下遍历。
你想想看,如果嵌套了七八层布局,每个子View都要计算自己的位置和大小。这就像你在一栋楼里找房间,每层都要停下来问路。能不慢吗?
所以优化的核心思路就两条:减少层级、延迟加载。
核心原则:能平铺就别嵌套,能延迟就别提前加载。
一、减少布局层级:别让ViewGroup“套娃”
我刚开始写Android时,特别喜欢用LinearLayout。一个页面从上到下,左边右边,全是LinearLayout嵌套。后来用Hierarchy Viewer一看,好家伙,一个列表项居然有七八层。
减少层级最直接的办法,就是用RelativeLayout或ConstraintLayout替代多层LinearLayout。
举个例子,一个常见的“头像+用户名+描述”布局:
<!-- ❌ 不推荐:多层嵌套 -->
<LinearLayout android:orientation="horizontal">
<ImageView android:id="@+id/avatar" />
<LinearLayout android:orientation="vertical">
<TextView android:id="@+id/name" />
<TextView android:id="@+id/desc" />
</LinearLayout>
</LinearLayout>
<!-- ✅ 推荐:用RelativeLayout平铺 -->
<RelativeLayout>
<ImageView android:id="@+id/avatar"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true" />
<TextView android:id="@+id/name"
android:layout_toRightOf="@id/avatar"
android:layout_alignTop="@id/avatar" />
<TextView android:id="@+id/desc"
android:layout_toRightOf="@id/avatar"
android:layout_below="@id/name" />
</RelativeLayout>
我的经验:用Hierarchy Viewer或Layout Inspector检查布局层级。如果一个页面超过5层,就该考虑优化了。我一般控制在3层以内。
二、ViewStub:按需加载,别一股脑全显示
有些布局不是一开始就需要显示的。比如“加载失败”的提示、用户协议弹窗、或者某些高级功能面板。如果一启动就把它们全inflate出来,白白浪费了时间和内存。
ViewStub就是干这个的。它本身不占布局空间,只有当你调用inflate()或setVisibility(View.VISIBLE)时,才会真正加载。
<!-- 定义ViewStub -->
<ViewStub
android:id="@+id/stub_loading_error"
android:layout="@layout/layout_loading_error"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
// 在代码中按需加载
ViewStub stub = findViewById(R.id.stub_loading_error);
if (stub != null) {
View errorView = stub.inflate();
// 或者直接 stub.setVisibility(View.VISIBLE);
}
注意:ViewStub一旦inflate之后,就不再是ViewStub了,而是被替换成了实际的布局。所以第二次调用inflate会抛异常。我习惯在inflate之前先判空。
我曾经在一个新闻列表页里,用ViewStub延迟加载了“分享面板”和“评论输入框”。用户不点击分享按钮,这两个布局就不会创建。页面启动速度提升了将近30%。
三、Merge标签:去掉多余的根节点
Merge标签的作用很简单:当你用<include>引入一个布局时,去掉那个布局的根ViewGroup。
举个例子,你有一个公共的“标题栏”布局:
<!-- title_bar.xml -->
<LinearLayout xmlns:android="..."
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView android:id="@+id/title" />
<Button android:id="@+id/back" />
</LinearLayout>
如果你在另一个布局里用<include layout="@layout/title_bar" />,那就会多出一层LinearLayout。这层LinearLayout除了增加层级,没有任何意义。
用Merge优化后:
<!-- title_bar.xml 优化版 -->
<merge xmlns:android="...">
<TextView android:id="@+id/title" />
<Button android:id="@+id/back" />
</merge>
这样include进来的就是两个直接子View,少了一层嵌套。
适用场景:Merge标签只适合作为<include>的根标签,或者作为自定义ViewGroup的根布局。不能直接用在Activity的setContentView里。
四、ConstraintLayout:扁平化布局的利器
ConstraintLayout是Google推出的“终极扁平化方案”。它允许你用相对定位、链、屏障等约束关系,在一个平面内完成复杂布局。
以前用LinearLayout嵌套三层才能实现的效果,用ConstraintLayout一层就能搞定。
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView android:id="@+id/avatar"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/avatar"
android:layout_marginStart="12dp" />
<TextView android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/name"
android:layout_marginStart="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
你看,一个ConstraintLayout就搞定了。没有嵌套,没有多余的ViewGroup。
我个人习惯:新项目一律用ConstraintLayout作为根布局。除非页面极其简单(比如只有一个TextView),否则不用LinearLayout。ConstraintLayout配合layout_constraintDimensionRatio还能轻松实现比例缩放,这在适配不同屏幕时特别好用。
五、实战建议:优化不是一蹴而就的
布局优化这件事,我建议你分三步走:
- 先测量:用Layout Inspector看看当前页面的层级深度和渲染耗时。找到“重灾区”。
- 再优化:针对层级深的区域,用ConstraintLayout或RelativeLayout替换多层LinearLayout。对不常用的布局,用ViewStub延迟加载。
- 最后验证:优化完再测一次,看看渲染时间有没有降下来。我一般要求页面渲染时间控制在16ms以内(60fps)。
避坑指南:我曾经为了追求“绝对扁平”,把所有的View都塞进一个ConstraintLayout里。结果约束条件写了几十条,可读性极差,维护起来想哭。后来我学会了适度分组——用<Group>或<include>把逻辑相关的View包起来,既保持了扁平,又保证了可读性。
嗯,布局优化说白了就是“做减法”。少一层嵌套,就少一次测量;少一个View,就少一次绘制。把这些细节做好,你的App自然就流畅了。
公众号:蓝海资料掘金营,微信deep3321