4、布局优化(上):过度绘制(Overdraw)的检测与优化,Hierarchy Viewer与Layout Inspector的使用,减少布局层级

布局优化这个话题,说实话我每次做技术分享都会讲。为什么?因为太多App卡顿、掉帧,根子就在布局上。你想想看,一个界面打开,GPU在那拼命地画了又画,CPU在那反复测量布局,能不卡吗?

今天我们先聊过度绘制(Overdraw),这是布局优化里最直观、也最容易出成果的一个方向。

4.1 过度绘制:到底在画什么?

过度绘制,说白了就是同一个像素点被重复绘制了多次。比如你画了一个红色背景,又在上面盖了一个蓝色按钮,那这个按钮区域的像素就被画了两次。如果按钮上还有文字,那就是三次。

我见过最夸张的一个项目,某个列表项的item,过度绘制达到了6倍。也就是说GPU每帧要画6遍那个区域。你想想,手机屏幕60Hz刷新率,一帧只有16.6ms,你让GPU画6遍,不卡才怪。

核心原则:过度绘制倍数控制在2.5倍以内是比较健康的。超过3倍,基本就能感觉到卡顿了。

4.2 如何检测过度绘制?

Android系统自带了一个调试工具,叫「调试GPU过度绘制」。打开方式很简单:

  1. 进入开发者选项
  2. 找到「调试GPU过度绘制」
  3. 选择「显示过度绘制区域」

这时候你的App界面会变成五颜六色的。颜色代表什么?

颜色 含义 建议
无色 没有过度绘制 完美
蓝色 过度绘制1倍 可接受
绿色 过度绘制2倍 需要关注
粉色 过度绘制3倍 必须优化
红色 过度绘制4倍及以上 严重问题

我记得有一次接手一个老项目,打开过度绘制检测一看,整个界面红彤彤的,像过年一样。后来发现是每个Fragment都设置了背景色,Activity又设了背景,再加上一些自定义View的onDraw里重复绘制……嗯,那段时间我每天都在跟背景色较劲。

小技巧:检测过度绘制时,建议在真机上测试。模拟器的GPU渲染行为和真机差异很大,有时候模拟器上看着没问题,真机上就露馅了。

4.3 Hierarchy Viewer 与 Layout Inspector

过度绘制检测只是第一步,找到问题之后,你得知道布局结构长什么样。这时候就需要布局层级分析工具了。

4.3.1 Hierarchy Viewer(老牌工具)

Hierarchy Viewer是Android Studio早期版本自带的工具。它能以树形结构展示当前Activity的View层级。每个节点都标注了测量、布局、绘制三个阶段的耗时。

我个人习惯用它的「像素网格」视图,能直观看到每个View占用的区域。不过要注意,Hierarchy Viewer只能连接开发版或eng版的设备,普通手机连不上。这也是后来Google推出Layout Inspector的原因之一。

4.3.2 Layout Inspector(现代工具)

Layout Inspector是Hierarchy Viewer的升级版。在Android Studio里,Tools → Layout Inspector 就能打开。它有几个我很喜欢的功能:

  • 实时3D视图:可以旋转查看布局的立体结构,一眼就能看出哪些View是多余的
  • 属性面板:点击任意View,直接看到它的所有属性,包括margin、padding、背景色等
  • 组件树:清晰的树形结构,支持搜索和过滤

我曾经用Layout Inspector发现过一个隐藏很深的坑:某个TextView被嵌套了5层LinearLayout,每一层都设置了android:background。从代码上看不出来,但Layout Inspector一打开,那个红色的过度绘制区域就暴露了。

注意:Layout Inspector在Android Studio 4.0以上版本体验最好。低版本可能会有卡顿或显示不全的问题。

4.4 减少布局层级:实战技巧

检测工具用上了,问题也找到了,接下来就是动手优化。减少布局层级,我总结了几个屡试不爽的方法。

4.4.1 使用ConstraintLayout替代多层嵌套

这是最直接的方法。以前用LinearLayout + RelativeLayout嵌套四五层才能实现的布局,用ConstraintLayout一层就能搞定。

<!-- 优化前:多层嵌套 -->
<LinearLayout>
    <RelativeLayout>
        <ImageView />
        <TextView />
    </RelativeLayout>
    <LinearLayout>
        <Button />
        <Button />
    </LinearLayout>
</LinearLayout>

<!-- 优化后:一层ConstraintLayout -->
<androidx.constraintlayout.widget.ConstraintLayout>
    <ImageView app:layout_constraintTop_toTopOf="parent" />
    <TextView app:layout_constraintTop_toBottomOf="@id/image" />
    <Button app:layout_constraintLeft_toLeftOf="parent" />
    <Button app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

你看,从3层变成1层,测量和绘制的开销直接减少。我做过测试,一个复杂的列表项,从5层嵌套优化到2层,列表滑动帧率从45fps提升到了58fps。

4.4.2 使用merge标签合并布局

当你用引入一个布局文件时,默认会多一层父容器。用可以避免这个问题。

<!-- 被引用的布局:使用merge -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <Button android:id="@+id/btn_ok" />
    <Button android:id="@+id/btn_cancel" />
</merge>

<!-- 主布局中引用 -->
<LinearLayout>
    <include layout="@layout/merge_buttons" />
</LinearLayout>

这样,两个Button就直接成为LinearLayout的子View,中间没有多余的层级。

4.4.3 使用ViewStub延迟加载

有些布局不是一开始就需要显示的,比如错误提示、加载动画等。用ViewStub可以做到「用的时候才加载」,避免一开始就参与布局测量。

<ViewStub
    android:id="@+id/stub_error"
    android:layout="@layout/layout_error"
    android:inflatedId="@+id/error_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

// 需要时再加载
ViewStub stub = findViewById(R.id.stub_error);
stub.inflate();

我曾经在一个详情页里,用ViewStub优化了3个不常用的模块。页面首次加载时间从800ms降到了450ms。说白了,就是「按需加载」,别让用户为看不到的东西买单。

4.4.4 减少背景色的重复设置

这是过度绘制最常见的来源。很多开发者习惯在每个View上都设一个背景色,结果层层叠加,GPU累得够呛。

我的建议是:只在最顶层的容器上设置背景色。子View如果需要背景,用透明或只设前景。如果必须设背景,考虑用合并。

一个经验值:优化过度绘制时,优先处理红色和粉色区域。蓝色区域可以暂时不管,因为1倍过度绘制对性能影响很小。

4.5 避坑指南

最后分享几个我踩过的坑:

  • 不要盲目使用ConstraintLayout:虽然它层级少,但如果约束条件太复杂,测量阶段反而更耗时。我见过一个布局写了30多个约束,结果比嵌套LinearLayout还慢。
  • 注意自定义View的onDraw:如果你重写了onDraw,记得在绘制前用clipRect限制绘制区域。否则即使布局层级很少,也会产生过度绘制。
  • Fragment切换时的背景残留:我曾经遇到一个bug,Fragment切换后,旧Fragment的背景还显示着。后来发现是Activity的根布局没有设置背景,导致新旧Fragment的背景叠加了。

嗯,布局优化这件事,说白了就是「少画、少测、少嵌套」。工具给你了,方法也讲了,剩下的就是动手去改。下一章我们会聊布局优化的下半部分——异步布局和X2C等进阶技巧,到时候见。