性能和视图层次结构

View 对象层次结构的管理方式会显著影响应用的性能。本页介绍如何评估视图层次结构是否减慢了应用的速度,并且还提供了一些可能出现的问题的解决策略。

本页将重点介绍如何改进基于 View 的布局。如需了解如何提高 Jetpack Compose 性能,请参阅 Jetpack Compose 性能

布局和测量性能

渲染管道包含“布局和测量”阶段,系统在此阶段以适当的方式将相关项放置在视图层次结构中。此阶段的测量部分确定 View 对象的大小和边界,布局部分确定 View 对象在屏幕上的放置位置。

这两个管道阶段在处理每个视图或布局时,都会产生少量开销。在大多数情况下,此开销很小,不会明显影响性能。不过,当应用添加或移除 View 对象时(例如在 RecyclerView 对象回收或重用 View 对象时),此开销会变大。如果 View 对象需要考虑调整大小来满足其约束条件,此开销也会增加。例如,如果应用对包围文本的 View 对象调用 SetText()View 可能需要调整大小。

如果这类操作花费的时间太长,可能会导致帧无法在允许的 16 毫秒内完成渲染,从而造成丢帧并使动画变得卡顿。

由于您无法将这些操作移至工作器线程(您的应用必须在主线程上处理这些操作),因此最好的选择是对它们进行优化,使其花费尽可能少的时间。

管理复杂布局

Android 布局允许您将界面对象嵌套在视图层次结构中。这种嵌套也会增加布局开销。当应用处理布局对象时,也会对布局的所有子对象执行相同的处理。

对于复杂的布局,有时仅在系统第一次计算布局时才会产生开销。例如,当应用在 RecyclerView 对象中回收复杂的列表项时,系统需要列出所有对象。又如,细微的更改可以往上朝父级传播,直至到达不影响父级大小的对象为止。

布局耗时过长的一个常见原因是,View 对象的层次结构互相嵌套。每个嵌套的布局对象都会增加布局阶段的开销。层次结构越扁平,完成布局阶段所需的时间越少。

我们建议您使用布局编辑器创建 ConstraintLayout 而非 RelativeLayoutLinearLayout,因为这么做通常更高效,并且减少了布局的嵌套。不过,对于可通过 FrameLayout 实现的简单布局,我们建议您使用 FrameLayout

如果使用 RelativeLayout 类,则可通过使用未设权重的嵌套 LinearLayout 视图,以更低的开销达到同样的效果。但是,如果您使用的是嵌套的加权 LinearLayout 视图,则布局开销会高得多,因为它需要多次布局传递,如下一部分所述。

我们还建议使用 RecyclerView 而不要使用 ListView,因为前者可以回收各个列表项的布局,这种方式不仅效率更高,而且可以提升滚动性能。

Double Taxation

通常情况下,框架会在一次传递中执行布局或测量阶段。但在一些情况比较复杂的布局中,在最终放置元素之前,框架可能必须对层次结构中需要多次传递才能解析的部分执行多次迭代。必须执行不止一次“布局和测量”迭代的情况称为“Double Taxation”。

例如,当您使用 RelativeLayout 容器时(该容器允许您根据其他 View 对象的位置来放置 View 对象),框架会执行以下一系列步骤:

  1. 执行一次“布局和测量”传递。在此过程中,框架会根据每个子对象的请求计算该子对象的位置和大小。
  2. 结合此数据和对象的权重确定关联视图的恰当位置。
  3. 执行第二次布局传递,以最终确定对象的位置。
  4. 进入渲染过程的下一阶段。

视图层次结构的层次越多,潜在的性能损失就越大。

如前所述,ConstraintLayout 通常比除 FrameLayout 之外的其他布局更高效。前者不易发生多次布局传递,并且在很多情况下,无需嵌套布局。

RelativeLayout 以外的容器也可能会增加 Double Taxation。例如:

  • LinearLayout 视图设置为水平方向,可能会导致执行双重“布局和测量”传递。如果您添加 measureWithLargestChild,则垂直方向上也可能会发生双重“布局和测量”传递,因为在这种情况下,框架可能需要执行第二次传递才能正确解析对象的大小。
  • 虽然 GridLayout 也允许相对定位,但它通常会通过预处理子视图之间的位置关系来避免 Double Taxation。不过,如果布局使用权重或使用 Gravity 类来填充,则会失去该预处理带来的好处,当容器为 RelativeLayout 时,框架可能必须执行多次传递。

多次“布局和测量”传递未必会造成性能负担。但是,如果它们的位置错误,则可能会成为负担。您应该警惕容器存在以下任一情况:

  • 它是视图层次结构中的根元素。
  • 它下面有较深的视图层次结构。
  • 屏幕中填充了它的许多实例,类似于 ListView 对象中的子对象。

诊断视图层次结构问题

布局性能是一个涉及许多方面的复杂问题。以下工具可以帮助您确定性能瓶颈发生位置。其他一些工具提供的信息不那么明确,但也能提供有用的提示。

Perfetto

Perfetto 是一款提供性能相关数据的工具。您可以在 Perfetto 界面中打开 Android 跟踪记录。

GPU 渲染分析

设备端 GPU 渲染分析工具在搭载 Android 6.0(API 级别 23)及更高版本的设备上提供,可为您提供可靠的性能瓶颈相关信息。借助此工具,您可以了解“布局和测量”阶段在每一帧渲染上花费的时间。此数据可以帮助您诊断运行时性能问题,以及确定需要解决哪些“布局和测量”问题。

在其捕获的数据的图形表示中,GPU 渲染分析使用蓝颜色来代表布局时间。如需详细了解如何使用此工具,请参阅 GPU 渲染分析速度

Lint

Android Studio 的 Lint 工具可以帮助您了解视图层次结构中的低效问题。如需使用此工具,请依次选择 Analyze > Inspect Code,如图 1 中所示。

图 1. 在 Android Studio 中选择 Inspect Code

Android > Lint > Performance 下面显示了有关各种布局项目的信息。如需查看更多详情,可以点击各个项目将其展开,然后屏幕右侧的窗格将显示详细信息。图 2 显示了已展开的信息的示例。

图 2. 查看有关 Lint 工具发现的特定问题的信息。

点击某一项即可在右侧窗格中显示与其相关的问题。

如需详细了解该区域中显示的特定主题和问题,请参阅 Lint 文档。

布局检查器

Android Studio 的 Layout Inspector 工具提供关于应用视图层次结构的可视化表示。这是一种浏览应用层次结构的好方法,它提供关于特定视图父链的清晰的可视化表示,可帮助您检查应用构建的布局。

Layout Inspector 提供的视图还可以帮助您确定由 Double Taxation 引起的性能问题。它还可以让您通过一种方法来识别嵌套布局的深层链或具有大量嵌套子级的布局区域,这是导致性能下降的一个潜在因素。在这两种情况下,“布局和测量”阶段的开销可能会特别大,从而导致性能问题。

如需了解详情,请参阅使用布局检查器和布局验证工具调试布局

解决视图层次结构问题

如需解决由视图层次结构引起的性能问题,其背后的基本原理很简单,但实际操作起来却比较困难。防止因视图层次结构导致性能下降包括两个目标:一个是实现视图层次结构扁平化,一个是减少 Double Taxation。此部分探讨实现这两个目标的一些策略。

移除多余的嵌套布局

ConstraintLayout 是一个 Jetpack 库,包含可用于在布局中放置视图的众多不同机制。这样可以减少嵌套一个 ConstaintLayout 的需要,并且有助于扁平化视图层次结构。与其他布局类型相比,使用 ConstraintLayout 扁平化层次结构通常更简单。

开发者经常会过度使用嵌套布局。例如,可能会在 RelativeLayout 容器包含一个同样也是 RelativeLayout 容器的子级。这种嵌套是多余的,并且会给视图层次结构造成不必要的开销。Lint 可以为您标记此类问题,从而减少调试时间。

采用合并或包含

造成多余嵌套布局的一个常见原因就是 <include> 标记。例如,您可以定义一个类似如下的可重复使用的布局:

<LinearLayout>
    <!-- some stuff here -->
</LinearLayout>

然后,您可以添加 <include> 标记,将以下项添加到父级容器中:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/app_bg"
    android:gravity="center_horizontal">

    <include layout="@layout/titlebar"/>

    <TextView android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="@string/hello"
              android:padding="10dp" />

    ...

</LinearLayout>

该标记会将第一个布局嵌套在第二个布局中,而此嵌套是不必要的。

<merge> 标记可以避免此问题。如需了解此标记,请参阅使用 <merge> 标记

采用开销较低的布局

您可能无法调整现有的布局方案,使其不包含多余的布局。在某些情况下,唯一的解决方案可能是,通过切换到完全不同的布局类型来实现层次结构的扁平化。

例如,您可能会发现,TableLayout 作为具有许多位置依赖项的更复杂的布局,可以提供相同的功能。Jetpack 库 ConstraintLayout 提供的功能与 RelativeLayout 类似,还有其他功能方便您创建更扁平、更高效的布局。