4. Choreographer机制
布局层级,说白了就是 View 树的深度。我见过太多座舱项目,一个简单的仪表盘界面,嵌套了七八层 LinearLayout。你想想看,每多一层嵌套,CPU 就要多走一次 measure/layout/draw 循环。在 60fps 的座舱屏幕上,这简直是灾难。
我个人习惯,拿到一个 UI 设计稿,第一件事不是写代码,而是先画 View 树结构图。把每个节点的父子关系理清楚,再动手。这习惯帮我省了不少后期优化的功夫。
4.1 嵌套层级为什么是性能杀手?
先看一个典型的反面教材。很多开发者习惯这么写:
<!-- 反面示例:6层嵌套 -->
<LinearLayout android:orientation="vertical">
<LinearLayout android:orientation="horizontal">
<LinearLayout android:orientation="vertical">
<TextView />
<TextView />
</LinearLayout>
<ImageView />
</LinearLayout>
<LinearLayout android:orientation="horizontal">
<Button />
<Button />
</LinearLayout>
</LinearLayout>
这段代码,每个 LinearLayout 都要经历一次 measure 和 layout。如果界面在 60fps 下运行,每帧都要重复这个过程。我在项目中遇到过,一个空调控制面板因为嵌套了 8 层,导致滑动时明显掉帧。去掉两层嵌套后,帧率直接从 45fps 升到了 58fps。
4.2 ConstraintLayout:扁平化的利器
ConstraintLayout 的出现,说白了就是为了解决嵌套问题。它用约束关系代替了嵌套结构。一个 ConstraintLayout 可以搞定以前需要三四个 LinearLayout 才能实现的布局。
看一个实际对比。上面那个 6 层嵌套的例子,用 ConstraintLayout 重写:
<!-- 优化后:1层ConstraintLayout -->
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView android:id="@+id/title" />
<TextView android:id="@+id/subtitle"
app:layout_constraintTop_toBottomOf="@id/title"/>
<ImageView android:id="@+id/icon"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintEnd_toEndOf="parent"/>
<Button android:id="@+id/btn_ok"
app:layout_constraintTop_toBottomOf="@id/subtitle"/>
<Button android:id="@+id/btn_cancel"
app:layout_constraintTop_toBottomOf="@id/subtitle"
app:layout_constraintStart_toEndOf="@id/btn_ok"/>
</androidx.constraintlayout.widget.ConstraintLayout>
嗯,这里要注意:ConstraintLayout 虽然扁平,但约束链如果太复杂,反而会增加 measure 阶段的复杂度。我建议每个 ConstraintLayout 内的子 View 数量控制在 15 个以内。超过这个数,考虑拆分成多个 ConstraintLayout 或者使用 Group。
| 布局类型 | 嵌套深度 | measure 耗时 (ms) | layout 耗时 (ms) | draw 耗时 (ms) |
|---|---|---|---|---|
| LinearLayout (6层) | 6 | 2.8 | 1.5 | 0.9 |
| RelativeLayout (3层) | 3 | 1.6 | 0.8 | 0.7 |
| ConstraintLayout (1层) | 1 | 0.9 | 0.4 | 0.5 |
数据来自我实际在骁龙 8155 平台上的测试。可以看到,ConstraintLayout 在 measure 阶段优势明显。
4.3 避免过度绘制
过度绘制,就是同一个像素点被重复绘制了多次。在座舱仪表盘上,最常见的场景是:背景色、卡片背景、文字背景层层叠加。
我曾经调试过一个导航界面,打开开发者选项里的「显示过度绘制区域」,整个屏幕都是深红色(4x 过度绘制)。排查下来,发现是每个 View 都设置了背景色,而父布局也设置了背景色。
避免过度绘制的几个实用技巧:
- 移除不必要的背景: 如果父布局已经设置了背景色,子 View 不需要再设置相同的背景色。
- 使用 ViewStub 延迟加载: 对于非首屏显示的复杂布局,用 ViewStub 代替 include。等真正需要时再 inflate。
- 合并绘制层级: 能用自定义 View 绘制的内容,就不要拆成多个 View 叠加。
- 使用 clipChildren 和 clipToPadding: 减少不必要的裁剪区域。
4.4 布局层级优化的核心流程
下面这张图,是我在项目中总结的布局优化决策流程。每次做新功能,我都会按这个流程走一遍。
4.5 实战中的几个小技巧
最后分享几个我在座舱项目中常用的技巧:
- 使用 Hierarchy Viewer 或 Layout Inspector: 这两个工具能直观看到 View 树的深度和每个节点的耗时。我每次性能调优,第一步就是打开它们。
- 合理使用 merge 标签: 在 include 布局时,用 merge 标签可以减少一层不必要的父布局。
- 避免在 measure 阶段做耗时操作: 不要在 onMeasure 里做图片解码、网络请求等操作。这些应该提前准备好。
- 使用 ConstraintLayout 的 Barrier 和 Group: 这两个辅助工具能帮你减少约束链的复杂度,间接提升 measure 效率。
布局层级优化,说白了就是「做减法」。去掉不必要的嵌套,合并重复的绘制,用更扁平的结构替代复杂的层级。在座舱这种对实时性要求极高的场景下,每一毫秒的节省,都可能意味着一个流畅的动画和一个卡顿的动画之间的差别。