欢迎参加我们将于 6 月 3 日举行的 #Android11:Beta 版发布会

使用 GPU 渲染模式分析工具进行分析

GPU 渲染模式分析工具可以显示渲染流水线的每个阶段渲染前一帧所用的相对时间。这些信息有助于您确定流水线中的瓶颈所在,从而了解应该优化哪些方面来提高应用的渲染性能。

本页将简要介绍每个流水线阶段会进行的操作,并讨论可能会导致该阶段出现瓶颈的问题。在阅读本页之前,您应该熟悉 GPU 渲染模式分析一节中的信息。此外,为了理解各个阶段是如何配合工作的,查看渲染流水线的工作原理会有所帮助。

视觉呈现

GPU 渲染模式分析工具以图表(以颜色编码的直方图)的形式显示各个阶段及其相对时间。图 1 显示了此图表的一个示例。

图 1. GPU 渲染模式分析图表

GPU 渲染模式分析图表中显示的每个竖条中的每个分段都表示流水线的一个阶段,并在条形图中使用特定的颜色突出显示。图 2 说明了显示的每种颜色所代表的含义。

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

了解每种颜色所代表的含义后,就可以针对应用的特定方面来优化其渲染性能。

阶段及其含义

本部分说明与图 2 中的颜色对应的每个阶段发生的操作,以及需要注意的瓶颈原因。

输入处理

流水线的输入处理阶段测量的是应用处理输入事件所用的时间。该指标表示应用执行输入事件回调给出的代码所用的时间。

当此分段较长时

此区域中的值较高通常是由于输入处理程序事件回调中的工作过多或过复杂导致的。 由于这些回调总是发生在主线程上,因此该问题的解决方法主要是直接优化工作,或者将工作转移到其他线程。

另外值得注意的是,此阶段可能出现 RecyclerView 滚动。当 RecyclerView 处理轻触事件时,它会立即滚动。因此,它可能会膨胀或填充新的项目视图。由于这个原因,尽可能加快此操作的执行速度就非常重要。TraceView 或 Systrace 等分析工具可以帮助您进一步调查相关问题。

动画

动画阶段显示的是评估在该帧中运行的所有 Animator 所用的时间。最常见的 Animator 有 ObjectAnimatorViewPropertyAnimator转换

当此分段较长时

造成此区域中的值较高的原因,通常是因动画的某种属性更改而执行的工作。例如,投掷动画会滚动 ListViewRecyclerView,从而导致大量的视图膨胀和填充。

测量/布局

为了在屏幕上绘制视图项,Android 会在视图层次结构的布局和视图中执行两项特定操作。

首先,系统会测量视图项。每个视图和布局都包含描述对象在屏幕上的尺寸的具体数据。有些视图具有确切的尺寸,而有些视图的尺寸则可以根据父级布局容器的尺寸进行调整。

然后,系统会对视图项进行布局。系统计算出子视图的尺寸后,就可以进行布局,调整视图在屏幕上的尺寸和位置。

系统不仅会对要绘制的视图进行测量和布局,还会逐层向上对这些视图的父级层次结构执行测量和布局,一直到根视图。

当此分段较长时

如果您的应用在这方面对每帧花费的时间较多,通常是由于需要布局的视图数量过多,或者出现了其他问题(例如在层次结构的错误位置发生了 Double Taxation 等)。在这两种情况下,要解决性能问题,都需要改善视图层次结构的性能

此外,添加到 onLayout(boolean, int, int, int, int)onMeasure(int, int) 的代码也可能会导致性能问题。TraceviewSystrace 可以帮助您检查调用堆栈,以找出代码可能存在的问题。

绘制

绘制阶段将视图的渲染操作(如绘制背景或文本)转换为一系列原生绘制命令。系统会将这些命令捕获到一个显示列表中。

“绘制”条记录了将这些命令捕获到显示列表中所用的时间,包括这一帧中需要在屏幕上更新的所有视图对应的命令。测量的时间涵盖了您添加到应用中的界面对象的所有代码。此类代码的示例包括 onDraw()dispatchDraw() 以及属于 Drawable 类的子类的各种 draw ()methods

当此分段较长时

简单地说,您可以将该指标理解为针对每个失效视图运行所有 onDraw() 调用所用的时间。该测量值涵盖了将绘制命令分配给可能存在的子项和可绘制对象所用的任何时间。因此,当您看到此竖条出现峰值时,可能是因为大量视图突然失效了。失效意味着必须重新生成视图的显示列表。或者,一些自定义视图的 onDraw() 方法中存在某种极其复杂的逻辑,也可能会导致此时间很长。

同步/上传

“同步和上传”指标表示在当前帧中将位图对象从 CPU 内存传输到 GPU 内存所需的时间。

作为两种不同的处理器,CPU 和 GPU 将不同的 RAM 区域用于执行处理。当您在 Android 中绘制位图时,系统会先将位图传输到 GPU 内存,然后 GPU 才能将其渲染到屏幕上。之后,GPU 会缓存位图,以便系统无需再次传输数据,除非纹理被移出 GPU 纹理缓存。

注意:在 Lollipop 设备上,此阶段为紫色。

当此分段较长时

帧的所有资源都必须位于 GPU 内存中才能用来绘制帧。这意味着,该指标的值较高可能表示需要加载大量小型资源或少量大型资源。一种常见的情况是应用要显示一个接近屏幕尺寸的位图。另一种情况是应用要显示大量缩略图。

要减小该值,您可以采用以下技巧:

  • 确保位图的分辨率不会比位图的显示尺寸大很多。例如,您的应用应避免将 1024x1024 的图片显示为 48x48 的图片。
  • 利用 prepareToDraw() 在下一个同步阶段之前异步预上传位图。

发出命令

“发出命令”分段表示的是发出将显示列表绘制到屏幕上所需的全部命令所需的时间。

为了将显示列表绘制到屏幕上,系统会向 GPU 发送必要的命令。通常,系统会通过 OpenGL ES API 执行此操作。

此过程需要一些时间,因为在将命令发送给 GPU 之前,系统会对每个命令执行最后的转换和裁剪。然后 GPU 会计算最终的命令,使得 GPU 端的开销增加。这些命令包括最后的转换和额外的裁剪。

当此分段较长时

在此阶段花费的时间直接反映了系统在给定帧中渲染的显示列表的复杂程度和数量。例如,如果有大量绘制操作,特别是在每个绘制基元都有较少的固有开销的情况下,将会使得这项时间增加。 例如:

Kotlin

    for (i in 0 until 1000) {
        canvas.drawPoint()
    }
    

Java

    for (int i = 0; i < 1000; i++) {
        canvas.drawPoint()
    }
    

发出以上命令的开销会远远大于发出以下命令的开销:

Kotlin

    canvas.drawPoints(thousandPointArray)
    

Java

    canvas.drawPoints(thousandPointArray);
    

发出命令与实际绘制显示列表之间并不总是存在 1 对 1 的关系。“发出命令”阶段捕获的是将绘制命令发送到 GPU 所用的时间,而“绘制”指标表示的是将已发出的命令捕获到显示列表所用的时间。

之所以会出现这种差异,是因为系统会尽可能地缓存显示列表。因此,在某些情况下,滚动、转换或动画会要求系统重新发送显示列表,但不必实际重新构建它(即重新捕获绘制命令)。因此,您可能会看到“发出命令”条较高,但“绘制命令”条并不高。

处理/交换缓冲区

当 Android 将其所有显示列表提交给 GPU 后,系统会发出最后一条命令,告诉图形驱动程序它已完成当前帧的处理。此时,驱动程序即可将更新后的图像显示到屏幕上。

当此分段较长时

有一点必须要注意:GPU 与 CPU 是并行工作的。Android 系统向 GPU 发出绘制命令,然后继续执行下一个任务。GPU 从队列中读取并处理这些绘制命令。

如果 CPU 发出命令的速度快于 GPU 处理命令的速度,这两个处理器之间的通信队列就会被占满。出现这种情况时,CPU 会阻塞并等待,直到队列中有位置来放置下一个命令。这种队列占满状态通常出现在“交换缓冲区”阶段,因为此时已提交了整个帧的命令。

缓解此问题的关键是降低 GPU 工作的复杂度,就像您在“发出命令”阶段所做的那样。

其他

除了渲染系统执行其工作所用的时间外,主线程上还会执行一些与渲染无关的工作。这些工作耗费的时间被报告为“其他时间”。“其他时间”通常表示可能在两个连续渲染帧之间的界面线程上执行的工作。

当此分段较长时

如果该值很高,则表示您的应用可能包含应在其他线程上执行的回调、Intent 或其他工作。通过方法跟踪Systrace 等工具可以查看主线程上运行的任务。此信息可帮助您有针对性地改进性能。