(十二)Android 性能优化 Gpu Overdraw & Gpu Rending

1,331 阅读16分钟

笔杆没多重,不写拿不动。

为什么要检查 GPU 渲染和绘制?

 Android 包含一些设备上的开发者选项,可直观地查看您的应用可能会在何处遇到界面渲染问题,如执行不必要的渲染工作,或执行长时间的线程和 GPU 操作。

 您的应用有一个重要的方面会影响用户对质量的感知,那就是应用将图片和文字渲染到屏幕上的流畅度。当应用正在绘制到屏幕时,请务必避免出现卡顿和响应迟缓的情况。

为什么要分析 GPU 渲染速度?

  • GPU 渲染模式分析工具以滚动直方图的形式直观地显示渲染界面窗口帧所花费的时间(以每帧 16 毫秒的速度作为对比基准)。
  • 在性能较低的 GPU 上,可用的填充率(GPU 填充帧缓冲区的速度)可能很低。随着绘制一帧所需的像素数的增加,GPU 可能需要更长的时间来处理新命令,并要求系统的其余任务等待,直到它跟上进度。此分析工具可帮助您确定 GPU 何时因尝试绘制像素而不堪重负,或何时因大量的过度绘制而被拖累。
  • 注意:此分析工具不适用于使用 NDK 的应用。这是因为每当 OpenGL 采用全屏上下文时,系统都会将框架消息推送到后台。在这种情况下,您可能会发现 GPU 制造商提供的分析工具很有帮助。

为什么要分析 GPU 过度绘制?

 发者选项中的另一个功能,通过对您的界面进行彩色编码来帮助您识别过度绘制。当您的应用在同一帧中多次绘制相同像素时,便会发生过度绘制。因此,此图可显示您的应用可能在何处执行不必要的渲染工作,这可能是 GPU 多此一举地渲染用户不可见的像素所导致的性能问题。

实操体验

(1)启用分析器

 启用分析器(GPU 渲染)
 开始前,请确保您使用的是搭载 Android 4.1(API 级别 16)或更高版本的设备,并启用开发者选项。如需在使用应用时开始分析设备 GPU 渲染,请执行以下操作:

  1. 在您的设备上,转到 Settings 并点按 Developer Options。
  2. 在 Monitoring 部分,选择 Profile GPU Rendering。
  3. 在“GPU 渲染模式分析”对话框中,选择在屏幕上显示为竖条,以在设备的屏幕上叠加图形。
  4. 打开您要分析的应用。

GPU 渲染模式分析图形

下面是有关输出的几点注意事项:

  • 对于每个可见应用,该工具将显示一个图形。
  • 沿水平轴的每个竖条代表一个帧,每个竖条的高度表示渲染该帧所花的时间(以毫秒为单位)。
  • 水平绿线表示 16 毫秒。要实现每秒 60 帧,代表每个帧的竖条需要保持在此线以下。当竖条超出此线时,可能会使动画出现暂停。
  • 该工具通过加宽对应的竖条并降低透明度来突出显示超出 16 毫秒阈值的帧。
  • 每个竖条都有与渲染管道中某个阶段对应的彩色区段。区段数因设备的 API 级别不同而异。

下表介绍了使用运行 Android 6.0 及更高版本的设备时分析器输出中某个竖条的每个区段。

GPU 渲染模式分析图表的图例

GPU 竖条区段含义

注意:尽管此工具名为“GPU 渲染模式分析”,但所有受监控的进程实际上发生在 CPU 中。通过将命令提交到 GPU 触发渲染,GPU 异步渲染屏幕。在某些情况下,GPU 可能会有太多工作要处理,所以您的 CPU 必须先等待一段时间,然后才能提交新命令。如果发生这种情况,您将看到橙色竖条和红色竖条上出现峰值,且命令提交将被阻止,直到 GPU 命令队列中腾出更多空间。

(2)直观呈现过度绘制

 执行下面操作打开GPU过度绘制显示:

  1. 在设备上,转到 Settings 并点按 Developer Options。
  2. 向下滚动到硬件加速渲染部分,并选择调试 GPU 过度绘制。
  3. 在调试 GPU 过度绘制对话框中,选择显示过度绘制区域。

Gpu 某个应用正常时的样子(左侧),以及它在 GPU 过度绘制后的样子(右侧)

 Android 将按如下方式为界面元素着色,以确定过度绘制的次数:

Gpu 过度绘制颜色对应次数

 请注意,这些颜色是半透明的,因此在屏幕上看到的确切颜色取决于界面内容。
 请注意,有些过度绘制是不可避免的。在优化您的应用的界面时,应尝试达到大部分显示真彩色或仅有 1 次过度绘制(蓝色)的视觉效果。

Gpu 大量过度绘制的应用(左侧)以及很少过度绘制的应用(右侧)的示例

(3)减少过度绘制

 应用可能会在单个帧内多次绘制同一个像素,这种情况称为“过度绘制”。过度绘制通常是不必要的,最好避免。它会浪费 GPU 时间来渲染与用户在屏幕上所见内容无关的像素,进而导致性能问题。

(3.1)关于过度绘制

  过度绘制是指系统在渲染单个帧的过程中多次在屏幕上绘制某一个像素。例如,如果我们有若干界面卡片堆叠在一起,每张卡片都会遮盖其下面一张卡片的部分内容。
  但是,系统仍然需要绘制堆叠中的卡片被遮盖的部分。这是因为堆叠的卡片是根据 Painter 算法(也就是按从后到前的顺序)来渲染的。按照这种渲染顺序,系统可以将适当的透明度混合应用于阴影之类的半透明对象。

注意:过度绘制的问题在 Google I/O 大会性能会议中讨论过,这个问题现在已没有当时那么严重了。这是因为低端设备的 GPU 性能不断提升,而其显示屏的分辨率保持在了一个相对较低的水平。除非是要针对已知的低性能 GPU 设备进行优化,否则建议将重点放在优化界面线程工作上,以确保应用性能稳定。除此之外,在很多情况下,操作系统优化可以避免应用内的过度绘制(例如,Fragment 背景过度绘制窗口背景)。

(3.2)找出过度绘制问题

  • GPU 过度绘制调试工具:GPU 过度绘制调试工具使用颜色编码来显示应用在屏幕上绘制每个像素的次数。此计数越高,过度绘制影响应用性能的可能性越大。
  • GPU 渲染模式分析工具:GPU 渲染模式分析工具以滚动直方图的形式显示渲染流水线的每个阶段显示一帧所用的时间。每个竖条的“处理”部分都以橙色表示,它显示系统何时交换缓冲区;该指标可提供有关过度绘制的重要线索。

(3.3)解决过度绘制问题

  您可以采取以下几种策略来减少甚至消除过度绘制:

移除布局中不需要的背景

  • 1.如果父控件有颜色,也是自己需要的颜色,那么就不必在子控件加背景颜色
  • 2.如果每个自控件的颜色不太一样,而且可以完全覆盖父控件,那么就不需要再父控件上加背景颜色
  • 3.一个应用如果有自己的主色调,大多数页面的背景都是这个色值,则可考虑把app的windowBackground设置为此色值,页面中的layout布局不要设置背景色

 默认情况下,布局没有背景,这表示布局本身不会直接渲染任何内容。但是,当布局具有背景时,其有可能会导致过度绘制。
 移除不必要的背景可以快速提高渲染性能。不必要的背景可能永远不可见,因为它会被应用在该视图上绘制的任何其他内容完全覆盖。例如,当系统在父视图上绘制子视图时,可能会完全覆盖父视图的背景。

使视图层次结构扁平化

 可以通过优化 视图层次结构 来减少重叠界面对象的数量,从而提高性能。

  借助先进的布局设计方法,您可以轻松对视图进行堆叠和分层,从而打造出精美的设计。但是,这样做会导致过度绘制,从而降低性能,特别是在每个堆叠视图对象都是不透明的情况下,这需要将可见和不可见的像素都绘制到屏幕上。

减少使用透明度

  如果能使用不透明色值实现的,尽量不要加透明度,比如要实现通过设置80%不透明度黑色来实现一种灰色,远不如直接用不透明的灰色色值性能好。

 在屏幕上渲染透明像素,即所谓的透明度渲染,是导致过度绘制的重要因素。在普通的过度绘制中,系统会在已绘制的现有像素上绘制不透明的像素,从而将其完全遮盖,与此不同的是,透明对象需要先绘制现有的像素,以便达到正确的混合效果。诸如透明动画、淡出和阴影之类的视觉效果都会涉及某种透明度,因此有可能导致严重的过度绘制。您可以通过减少要渲染的透明对象的数量,来改善这些情况下的过度绘制。例如,如需获得灰色文本,您可以在 TextView 中绘制黑色文本,再为其设置半透明的透明度值。但是,您可以简单地通过用灰色绘制文本来获得同样的效果,而且能够大幅提升性能。

(4)性能与视图层次结构

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

(4.1)布局(layout)和度量(measure)性能

  • 渲染流水线包含“布局和度量”阶段,系统在此阶段以适当的方式将相关项放置在视图层次结构中。此阶段的度量部分确定View对象的大小和边界,布局部分确定 View对象在屏幕上的放置位置。
  • 这两个流水线阶段在处理每个视图或布局时,都会产生少量开销。在大多数情况下,此开销很小,不会明显影响性能。不过,当应用添加或移除 View 对象时(例如在 RecyclerView 对象回收或重用 View 对象时),此开销会变大。如果  View 对象需要考虑调整大小来保持其约束,此开销也会增加:例如,如果应用对包围文本的  View 对象调用 SetText()View 可能需要调整大小。
  • 如果这类操作花费的时间太长,可能会导致帧无法在允许的 16 毫秒内完成渲染,从而造成丢帧并使动画变得粗糙。
  • 由于您无法将这些操作移至工作器线程(您的应用必须在主线程上处理这些操作),因此最好的选择是对它们进行优化,使其花费尽可能少的时间。

(4.2)管理复杂性:布局很重要

  • Android 布局允许您将界面对象嵌套在视图层次结构中。这种嵌套也会增加布局开销。当应用处理布局对象时,也会对布局的所有子对象执行相同的处理。对于复杂的布局,有时仅在系统第一次计算布局时才会产生开销。例如,当应用在 RecyclerView 对象中回收复杂的列表项时,系统需要列出所有对象。又如,细微的更改可以往上朝父级传播,直至到达不影响父级大小的对象为止。
  • 布局耗时过长最常见的情况是,View 对象的层次结构互相嵌套。每个嵌套的布局对象都会增加布局阶段的开销。层次结构越扁平,完成布局阶段所需的时间越少。
  • 如果使用 RelativeLayout 类,则可通过使用未设权重的嵌套 LinearLayout 视图,以更低的开销达到同样的效果。此外,如果您的应用以 Android 7.0(API 级别 24)为目标平台,您或许可以使用特殊的布局编辑器来创建 ConstraintLayout对象,而非 RelativeLayout。这样做可以避免本节中讲到的许多问题。ConstraintLayout类提供类似的布局控制,但性能大大提高。该类使用自己的约束解析系统,采用与标准布局完全不同的方式来解析视图之间的关系。

(4.3)Double Taxation(双重“布局和度量”)

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

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

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

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

4.3.2 此外,RelativeLayout 以外的容器也可能会导致 Double Taxation。例如:

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

4.3.3 多次“布局和度量”遍历本身并不是性能负担,但如果发生在错误的地方,就可能会变成负担。您应该警惕容器存在以下情况:

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

(4.4)诊断视图层次结构问题

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

(4.5)解决视图层次结构问题

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

移除多余的嵌套布局

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

采用 merge/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>

 该 include 会将第一个布局嵌套在第二个布局中,而此嵌套是不必要的。merge 标记可以避免此问题。如需了解此标记,请参阅通过 重复使用布局

采用开销较低的布局

 您可能无法调整现有的布局方案,使其不包含多余的布局。在某些情况下,唯一的解决方案可能是,通过切换到完全不同的布局类型来实现层次结构的扁平化。
  例如,您可能会发现,TableLayout 作为具有许多位置依赖项的更复杂的布局,可以提供相同的功能。在 Android 的 N 版本中,ConstraintLayout 类提供了与 RelativeLayout 类似的功能,但开销要低得多。

按需加载(视图加载延迟)

  有时,您的布局可能需要很少使用的复杂视图。无论是作品详情、进度指示器还是撤消消息,您都可以通过仅在需要时加载这些视图来减少内存使用量并加快渲染速度。
  如果您具有应用将来可能需要的复杂视图,则可以使用延迟加载资源这项重要的方法。您可以通过为复杂且很少使用的视图定义 ViewStub 来实现此方法。

 定义 ViewStub

    <ViewStub
        android:id="@+id/stub_import"
        android:inflatedId="@+id/panel_import"
        android:layout="@layout/progress_overlay"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom" />
    

 加载 ViewStub 布局

    findViewById(R.id.stub_import).setVisibility(View.VISIBLE);
    // or
    View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
    

一起来查看自己项目的 Gpu 现状吧~

小编的扩展链接

参考链接

一顾倾人城,再顾倾人国

   ❤ 比心 ❤