关于过度绘制和渲染的介绍

1.引起卡顿的原因

在Android中,UI渲染太慢和过度绘制是造成卡顿的主要原因。

关于UI渲染:

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。

那么,如果每一帧的渲染时间超过了16ms,就会产生丢帧,导致卡顿现象。在Android中,执行复杂动画、绘制或裁剪过大图片 、频繁刷新UI等操作都是发生这些现象的原因。

关于过度绘制:

屏幕上的某个像素在同一帧的时间内被绘制了多次。

如果一个布局的视图之间层级嵌套太多,或者每个视图都有自己的背景就会导致“屏幕上的某个像素在同一帧的时间内被绘制了多次“。


2.分析优化

UI渲染分析

精确定为到导致卡顿现象的代码特别困难,但可以通过一些工具来分析。

GPU呈现模式分析可以通过视觉化看到渲染UI时遇到的问题,如执行了更多的渲染工作,或执行了时间较长的线程和GPU操作。

它主要是在屏幕上显示滚动的柱状图,从视觉上可以看到渲染UI窗口的帧相对于每帧16ms所花费的时间。

它的开关在设置的开发者选项中,如下图所示:


上面图中呈现的彩色部分需要系统6.0以上才能显示,它说明了:
  • 水平绿色横线代表16ms。要达到每秒60帧,每帧的条形图应该保持在16ms以下。条形图在任何时候超过了这条线,都有可能暂停动画。
  • 每个条形图都有着色的组件,且这些元素映射到渲染管道中的一个阶段。
  • 每个条形图表示每一帧渲染的时间,条形图越高表示花费的渲染时间越长。

不同颜色条形图表示的意思是:

条形图组件 渲染阶段 描述
Swap Buffers 表示CPU等待CPU完成它的工作的时间。如果这个条形图变得很高,意味着应用程序在GPU上做了太多的工作。
Command Issue 表示Android 2D渲染器发布命令到OpenGL上绘制和重绘display list所需的时间。这个条形图的高度与执行每一个display list的时间总和成正比。
Sync& Upload 表示bitmap上传到GPU所用的时间。一大段预示着应用程序将要花费相当多的时间去加载大量图形。
Draw 表示用于创建和更新视图display list所用的时间。如果这个条形图很高,说明这里可能有很多自定义视图,或者在onDraw方法中执行了很多工作。
Measure / Layout 表示在视图层级的onLayout和onMeasure回调中花费了大量时间。一大段预示着试图层级需要很长时间来处理。
Animation 表示在评估所有在帧中运行的动画所花费的时间。如果这部分很大,可能是应用程序使用了一个自定义性能不是很好的动画,或者由于更新了属性而发生了一些意外的工作。
Input Handling 表示应用程序在执行输入事件回调方法中的代码所用的时间。如果这部分很大,预示着应用程序花费了大量时间在用户输入上。可以考虑分放在不同的线程中处理。
Misc Time / VSync Delay 表示应用程序在连续两帧之间执行操作所用的时间。预示着可以把UI线程中的大量处理分放在不同的线程中。

渲染阶段的意思是:

Input Handing:

管道的输入处理阶段测量了应用程序花费在处理输入事件需要的时间。这个度量标准表明了应用程序执行输入事件回调方法中的代码所花费的时长。

当这部分很大时

在这个区域的值很高,通常是由于太多的工作或太复杂的工作发生在输入处理事件回调方法中。因为这些回调方法总发生在主线程里面,要解决这些问题,可以直接优化这些工作,或分放这些工作到不同的线程中。

值得注意的是,RecyclerView在滚动的时候会出现在这个阶段。RecyclerView滚动时会立即消耗触摸事件。因此,它会填充或更新新的条目视图。所以,让这个操作尽可能的快是非常重要的。可以使用TraceView或Systrace来做进一步分析。

Animation

动画阶段显示了评估所有运行在帧的动画所花费的时间。最通常用的动画是ObjectAnimator, ViewPropertyAnimator, 和Transitions.

当这部分很大时

在这个区域的值很高,通常是由于动画的一些属性改变而执行的工作导致的。

Measurement/Layout

为了让Android绘制视图条目到屏幕上,在层级中从布局到视图会执行两个特别的操作。

首先,系统会测量视图条目。每一个视图和布局在屏幕上有描述对象大小的特殊数据。有些视图有特定的大小,其它一些有一个适应父布局容器大小的大小。

其次,系统会布局视图条目。一但系统计算完子View的大小,系统就会对屏幕上的视图进行布局、分级和定位。

系统不仅对要被绘制的视图进行布局和测量,而且也会对这些视图的父层级进行布局和测量,最后一直布局和测量到根视图。

当这部分很大时

如果应用程序在这个区域的每一帧花费了大量时间,通常是因为大量的视图需要被布局,或者在层级的错误地方出现了像执行多个布局和测量迭代的问题。在这两种情况下,解决性能需要改进视图层次结构的性能

添加在onLayout(boolean, int, int, int, int) 和 onMeasure(int, int)方法中的代码也会造成性能问题。TraceView和Systrace工具可以用来帮助检查这些回调方法,从而识别出代码中可能存在的问题。

Drawing

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

绘制条形图记录了完成捕获命令到display list中所花费的时间,以及这帧内在屏幕需要更新的所有视图。测量时间用于在应用程序中添加到UI对象里的所有代码。这些代码可能在 onDraw()方法、dispatchDraw()方法和属于Drawable类子类的所有draw ()方法里。

当这部分很大时

简单的说,这个衡量标准可以理解为每次刷新View运行onDraw()方法所花费的时间。这个测量包括分发绘制命令到子View和drawable(当有drawable)所花费的时间。出于这个原因,当看到这个条形图很突出时,可能是刷新大量的View造成的。刷新需要重新生成View显示列表。另外,在自定义View的onDraw()方法中有一些极其复杂的逻辑也会导致时间变长。

Sync/Upload

Sync & Upload的衡量标准表示在当前帧中将bitmap对象从CPU内存转移到GPU内存所花费的时间。

对于不同的处理器,CPU和GPU有不同的RAM区域来专门处理。当在Android上绘制bitmap时,在GPU渲染它到屏幕上之前,系统需要先将bitmap转移到GPU内存中。

注意:在Lollipop设备上,这个阶段是紫色的。

当这部分很大时

所有的资源在被绘制到帧中之前,需要先驻留到GPU内存中。这个衡量标准的值很高意味着加载了大量的小资源或者小量的大资源。一个常见的情况是,应用程序展示一张接近屏幕大小的bitmap。另一种情况是,展示大量的缩略图。

为了缩小这个条形图,可采取下面的方式:

  • 确保bitmap的分辨率不会比显示它的大小大很多。例如,避免在1024*1024大小上显示48*48的图片。
  • 在下一个同步阶段之前,利用 prepareToDraw()方法来异步预加载bitmap。

Issuing Commands

发布命令部分表示在屏幕上绘制display list发布所有命令所花费的时间。

对于屏幕绘制display list,它会发送必要的命令到GPU。通常,通过OpenGL ES API来执行此操作。

这个过程需要一些时间,在发送命令到GPU之前,系统会对每一个命令执行最后的转换和裁剪。在GPU中计算最后的命令还会引起额外的开销。这些命令包括最终转换和附加裁剪。

当这部分很大时

这个阶段所花费的时间是,给定帧内系统渲染的display list的复杂性和数量的直接测量。例如,有很多draw操作,尤其是在每一个原始draw中还有小的固定的开销的情况。例如:

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

比下面有更多昂贵的开销:

canvas.drawPoints(mThousandPointArray);

在发布命令和实际绘制display list之间并不总是是1:1的相关性。发布命令与捕获发送绘制命令到GPU所花费的时间与不一样的。Draw 衡量标准表示捕获发布命令到display list所花费的时间。

之所以出现这种差异,是因为display list会被系统尽可能的缓存。因此,在滚动、转换或动画情况下,需要系统重新发送display list,但不必实际创建它-从头开始重新捕获绘制命令。因此,看到的是很高的”Issue commands”条形图,而不是Draw命令条形图。

Processing/Swapping Buffers

一旦Android完成提交所有的display list到GPU,系统就发送最后一条命令告诉图形驱动器,当前帧已经完成。此时,系统最终可以显示更新后的图片到屏幕上。

当这部分很大时

理解GPU执行工作与CPU是线性的是非常重要的。Android系统发布绘制命令到GPU后,就会移到下一个任务。GPU从队列中读取这些绘制命令然后处理它们。

在CPU发布命令比GPU消耗命令快的情况下,在处理器之间的通讯队列会被填满。当发生这种情况,CPU会阻塞,然后等待一直到队列中有空间来放置下一个命令。填满队列通常发生在Swap Buffers阶段,因为此时,整个帧的值命令已经被提交。

减轻这个问题的关键在于减少发生在GPU上的工作复杂性,在”Issue Commands”所要做的工作也一样。

Miscellaneous

除了渲染系统执行它的工作要花费时间外,与渲染毫无关系发生在主线程中执行的一系列工作也需要花费时间。这个工作消耗的时间被称为Misc时间。Misc时间一般表示在两个连续渲染帧之间可能发生在UI线程中的工作时间。

当这部分很大时

如果这个值很高,可能是应用程序中有回调,意图或发生在其它线程中其它工作。Method TracingSystrace工具可以提供运行在主线程中任务的可见性。这些信息可以帮助实现性能改进目标。

从上面的解释,当看到如下示例图时:

可以发现在条形图较高的帧中是Animation和Misc Time / VSync Delay渲染阶段花费了大量时间,所以可以优化性能不是很好的动画和把同一时刻的复杂操作放到不同的线程中处理。


过度绘制分析

调试GPU过度绘制主要通过在屏幕上显示不同的颜色来分析,它的开关在设置的开发者选项中,如下图所示:

这个功能主要是通过在UI上显示不同的颜色来识别过度绘制。过度绘制发生在同一帧内绘制在同一像素上超过了一次。因此,这个可视化显示了应用程序可能做了很多额外的渲染工作,由于GPU努力渲染的并不是对用户可见的像素,所以这可能会成为一个性能问题。因此,应该尽可能的修复过度绘制事件

显示在UI上的不同颜色元素分别表示:

颜色 过度绘制
真实颜色 没有过度绘制
蓝色 过度绘制一次
绿色 过度绘制两次
粉红色 过度绘制三次
红色 过度绘制四次或者更多次

虽然过度绘制是不可避免的,但在优化应用程序的时候,应该尽量使显示的可视化颜色为真实颜色或者蓝色(过度绘制一次)。

### 修复过度绘制 这里有几种策略可以减少或者消除过度绘制: - 移除布局中不必要的背景 - 使视图层级扁平化 - 降低透明度 #### 移除布局中不必要的背景 默认情况下,一个布局没有背景,这意味着它自己不会直接地渲染任何东西。当布局有背景时,就有可能过度绘制。 移除不必要的背景是一种提升性能的快速方式。不必要的背景可能是从不可见的,因为它的背景完全会被应用程序绘制在最顶层的视图所覆盖。例如,当绘制的子视图在父视图之上时,系统可能完全覆盖父视图的背景。 想找到为什么过度绘制,可以使用[Layout Inspector](https://developer.android.com/studio/debug/layout-inspector.html)工具来遍历视图层级。当这样做时,可消除任何对用户不可见的背景。例如,许多容器共享一个背景,就可以消除不必要的背景(在应用程序中,设置窗口背景为主要的颜色背景,让在上面的其它所有容器都不定义背景值)。 使用[Layout Inspector](https://developer.android.com/studio/debug/layout-inspector.html)工具简单介绍: Layout Inspector 在Android Studio IDE中,可以在运行时检测应用程序的视图层级。它在菜单Tools的Android下面,如下图: ![这里写图片描述](https://img-blog.csdn.net/20171026175613215?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2FuZ2ppYW5nX3FpYW5tbw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 然后,再选择要检测的应用程序的进程。 Layout Inspector会捕获一张屏幕快照,在AndroidStudio中会呈现: - View树:在布局中的视图层级。 - 快照:有每一个View边界的屏幕快照。 - 属性表:选中View的布局属性。 ![这里写图片描述](https://img-blog.csdn.net/20171026180213525?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2FuZ2ppYW5nX3FpYW5tbw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) #### 使视图层级扁平化 现代布局使堆叠和分层视图更加容易。然而,这样做却会过度绘制,从而导致性能降低。特别在每一个堆叠视图对象是不透明的场景中,可见和不可见的像素都需要绘制在屏幕上。 如果遇到这类问题,可以通过[优化视图层次结构](https://developer.android.com/topic/performance/rendering/optimizing-view-hierarchies.html)来减少重叠的UI对象数量,从而提升性能。 要想扁平化布局效果,可以使用约束性布局[ConstraintLayout](https://constraintlayout.com/)。通过它,无论多么复杂的层级嵌套布局,最后都可以优化为下面这种结构:
<ViewGroup>
  <View />
  <View />
  <View />
  ...
  ...
</ViewGroup>

关于ConstraintLayout更多介绍,可以参看我的另外一篇博客
如何快速使用ConstraintLayout?

降低透明度

在屏幕上渲染透明像素,被称为alpha渲染,是过度绘制的一个重要因素。不像标准的过度绘制,通过在它们上面绘制不透明像素,系统会完全隐藏现有的绘制像素,为了出现混合平衡,透明对象需要现有的像素被首先绘制。像透明动画,淡入淡出,水滴阴影视觉效果,都涉及某种透明度,这会促成严重的过度绘制。可以通过减少渲染透明对象的数量来改善过度绘制。例如,可以通过在TextView中绘制有透明度alpha值的黑色文本来得到灰色文本。但是也可以通过绘制灰色文本以更好的性能达到相同效果。


总结

关于UI渲染和过度绘制就介绍和分析到这里。加快UI渲染需要在每一个渲染阶段进行优化,所涉及的方面较广,尤其在开发时要多注意和思考。另外,使用Method TracingSystrace工具可以帮助分析UI渲染问题。降低过度绘制 可以从移除布局中不必要的背景、使视图层级扁平化、降低透明度三方面来进行,编写应用程序应尽量使显示的可视化颜色为真实颜色或者蓝色(过度绘制一次)。

参考:
Profile GPU Rendering Walkthrough
Reducing Overdraw

猜你喜欢

转载自blog.csdn.net/wangjiang_qianmo/article/details/78331612