从 Android 3.0(API 级别 11)开始,Android 2D 渲染管道支持硬件加速,这意味着在 View
的画布上执行的所有绘图操作都使用 GPU。由于启用硬件加速所需的资源增加,您的应用将消耗更多 RAM。
如果您的目标 API 级别 >= 14,则默认情况下会启用硬件加速,但也可以显式启用。如果您的应用程序仅使用标准视图和 Drawable
,则全局启用它不会导致任何不良的绘制效果。但是,由于并非所有 2D 绘图操作都支持硬件加速,因此启用它可能会影响某些自定义视图或绘图调用。问题通常表现为不可见元素、异常或渲染错误的像素。为了解决这个问题,Android 允许您在多个级别启用或禁用硬件加速。请参阅 控制硬件加速。
如果您的应用程序执行自定义绘图,请在启用了硬件加速的实际硬件设备上测试您的应用程序,以查找任何问题。 对绘图操作的支持 部分描述了硬件加速的已知问题以及如何解决这些问题。
另请参阅 与框架 API 结合使用 OpenGL 和 Renderscript
控制硬件加速
您可以在以下级别控制硬件加速
- 应用程序
- 活动
- 窗口
- 视图
应用程序级别
在您的 Android 清单文件中,将以下属性添加到 <application>
标签以启用整个应用程序的硬件加速
<application android:hardwareAccelerated="true" ...>
活动级别
如果您的应用程序在全局启用硬件加速后表现不正常,您也可以为单个活动控制它。要在活动级别启用或禁用硬件加速,您可以使用 <activity>
元素的 android:hardwareAccelerated
属性。以下示例为整个应用程序启用了硬件加速,但为一个活动禁用了它
<application android:hardwareAccelerated="true"> <activity ... /> <activity android:hardwareAccelerated="false" /> </application>
窗口级别
如果您需要更细粒度的控制,您可以使用以下代码为给定窗口启用硬件加速
Kotlin
window.setFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED )
Java
getWindow().setFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
注意:您目前无法在窗口级别禁用硬件加速。
视图级别
您可以使用以下代码在运行时为单个视图禁用硬件加速
Kotlin
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
Java
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
注意:您目前无法在视图级别启用硬件加速。视图层除了禁用硬件加速之外还有其他功能。有关其用途的更多信息,请参阅 视图层。
确定视图是否已硬件加速
对于应用程序来说,了解它当前是否已硬件加速有时很有用,尤其是对于自定义视图等内容。如果您的应用程序执行大量自定义绘图,并且并非所有操作都得到新渲染管道的正确支持,这将特别有用。
有两种不同的方法可以检查应用程序是否已硬件加速
View.isHardwareAccelerated()
如果View
附加到硬件加速窗口,则返回true
。Canvas.isHardwareAccelerated()
如果Canvas
已硬件加速,则返回true
如果必须在绘图代码中执行此检查,请尽可能使用 Canvas.isHardwareAccelerated()
而不是 View.isHardwareAccelerated()
。当视图附加到硬件加速窗口时,它仍然可以使用非硬件加速的 Canvas 进行绘制。例如,当将视图绘制到位图以进行缓存时,就会发生这种情况。
Android 绘图模型
启用硬件加速后,Android 框架将利用一个新的绘图模型,该模型使用显示列表将您的应用程序渲染到屏幕上。为了完全理解显示列表及其对应用程序的影响,了解 Android 如何在没有硬件加速的情况下绘制视图也很有用。以下部分介绍了基于软件和硬件加速的绘图模型。
基于软件的绘图模型
在软件绘图模型中,视图使用以下两个步骤绘制
- 使层次结构失效
- 绘制层次结构
每当应用程序需要更新其 UI 的一部分时,它都会在内容已更改的任何视图上调用 invalidate()
(或其变体之一)。无效消息一直传播到视图层次结构的顶端,以计算需要重新绘制的屏幕区域(脏区域)。然后,Android 系统绘制与脏区域相交的层次结构中的任何视图。不幸的是,这种绘图模型有两个缺点
- 首先,此模型需要在每次绘制过程中执行大量代码。例如,如果您的应用程序在按钮上调用
invalidate()
,并且该按钮位于另一个视图之上,则 Android 系统会重新绘制该视图,即使它没有改变。 - 第二个问题是,绘图模型可以隐藏应用程序中的错误。由于 Android 系统会在视图与脏区域相交时重新绘制视图,因此您更改了内容的视图可能会被重新绘制,即使没有在它上面调用
invalidate()
。当这种情况发生时,您依赖于另一个视图的失效以获得正确的行为。这种行为可能会在您每次修改应用程序时发生变化。因此,您应该始终在修改影响视图绘图代码的数据或状态时,在自定义视图上调用invalidate()
。
注意:当 Android 视图的属性发生变化时,例如 TextView
中的背景颜色或文本,它们会自动调用 invalidate()
。
硬件加速绘图模型
Android 系统仍然使用 invalidate()
和 draw()
请求屏幕更新并渲染视图,但以不同的方式处理实际的绘制。Android 系统不会立即执行绘图命令,而是将它们记录在显示列表中,显示列表包含视图层次结构绘图代码的输出。另一个优化是,Android 系统只需要记录和更新被 invalidate()
调用标记为脏的视图的显示列表。可以通过重新发出先前记录的显示列表来简单地重新绘制未失效的视图。新绘图模型包含三个阶段
- 使层次结构失效
- 记录和更新显示列表
- 绘制显示列表
使用此模型,您不能依赖于与脏区域相交的视图来执行其 draw()
方法。为了确保 Android 系统记录视图的显示列表,您必须调用 invalidate()
。忘记这样做会导致视图在更改后仍然保持相同的外观。
使用显示列表也有利于动画性能,因为设置特定的属性(例如 alpha 或旋转)不需要使目标视图失效(它会自动完成)。这种优化也适用于具有显示列表的视图(当您的应用程序已硬件加速时,任何视图都是如此)。例如,假设有一个 LinearLayout
,它在 Button
上面包含一个 ListView
。 LinearLayout
的显示列表如下所示
- DrawDisplayList(ListView)
- DrawDisplayList(Button)
现在假设您要更改 ListView
的不透明度。在 ListView
上调用 setAlpha(0.5f)
后,显示列表现在包含以下内容
- SaveLayerAlpha(0.5)
- DrawDisplayList(ListView)
- Restore
- DrawDisplayList(Button)
没有执行 ListView
的复杂绘图代码。相反,系统只更新了更简单的 LinearLayout
的显示列表。在没有启用硬件加速的应用程序中,列表及其父级的绘图代码将再次执行。
对绘图操作的支持
在硬件加速的情况下,2D 渲染管道支持最常用的 Canvas
绘图操作,以及许多不太常用的操作。所有用于渲染附带 Android 的应用程序、默认小部件和布局以及常见的高级视觉效果(如反射和平铺纹理)的绘图操作都受支持。
下表描述了跨 API 级别各种操作的支持级别
首次支持的 API 级别 | ||||
Canvas | ||||
drawBitmapMesh()(颜色数组) | 18 | |||
drawPicture() | 23 | |||
drawPosText() | 16 | |||
drawTextOnPath() | 16 | |||
drawVertices() | 29 | |||
setDrawFilter() | 16 | |||
clipPath() | 18 | |||
clipRegion() | 18 | |||
clipRect(Region.Op.XOR) | 18 | |||
clipRect(Region.Op.Difference) | 18 | |||
clipRect(Region.Op.ReverseDifference) | 18 | |||
带旋转/透视的 clipRect() | 18 | |||
Paint | ||||
setAntiAlias()(用于文本) | 18 | |||
setAntiAlias()(用于线条) | 16 | |||
setFilterBitmap() | 17 | |||
setLinearText() | ✗ | |||
setMaskFilter() | ✗ | |||
setPathEffect()(用于线条) | 28 | |||
setShadowLayer()(除文本之外) | 28 | |||
setStrokeCap()(用于线条) | 18 | |||
setStrokeCap()(用于点) | 19 | |||
setSubpixelText() | 28 | |||
Xfermode | ||||
PorterDuff.Mode.DARKEN(帧缓冲区) | 28 | |||
PorterDuff.Mode.LIGHTEN(帧缓冲区) | 28 | |||
PorterDuff.Mode.OVERLAY(帧缓冲区) | 28 | |||
Shader | ||||
ComposeShader 内部的 ComposeShader | 28 | |||
ComposeShader 内部的相同类型的着色器 | 28 | |||
ComposeShader 上的局部矩阵 | 18 |
Canvas 缩放
硬件加速的 2D 渲染管道最初是为了支持未缩放的绘制而构建的,在较高的缩放值下,某些绘制操作的质量会显着下降。这些操作被实现为以 1.0 的比例绘制的纹理,并由 GPU 变换。从 API 级别 28 开始,所有绘制操作都可以毫无问题地进行缩放。
下表显示了实现更改以正确处理大比例缩放的时间。要缩放的绘制操作 | 首次支持的 API 级别 |
drawText() | 18 |
drawPosText() | 28 |
drawTextOnPath() | 28 |
简单形状* | 17 |
复杂形状* | 28 |
drawPath() | 28 |
阴影层 | 28 |
注意:“简单”形状是 drawRect()
、drawCircle()
、drawOval()
、drawRoundRect()
和 drawArc()
(useCenter=false)命令,这些命令使用没有 PathEffect 的 Paint 发出,并且不包含非默认连接(通过 setStrokeJoin()
/ setStrokeMiter()
)。上述图表中的“复杂”包含其他实例的这些绘制命令。
如果您的应用程序受到任何这些缺失功能或限制的影响,您可以通过调用 setLayerType(View.LAYER_TYPE_SOFTWARE, null)
为应用程序的受影响部分关闭硬件加速。这样,您仍然可以利用其他地方的硬件加速。有关如何在应用程序的不同级别启用和禁用硬件加速的更多信息,请参阅 控制硬件加速。
视图层
在所有版本的 Android 中,视图一直具有渲染到屏幕外缓冲区的能力,方法是使用视图的绘图缓存,或者使用 Canvas.saveLayer()
。屏幕外缓冲区或层有多种用途。您可以使用它们在动画复杂视图时获得更好的性能,或应用合成效果。例如,您可以使用 Canvas.saveLayer()
实现淡入淡出效果,以将视图暂时渲染到层中,然后以不透明度系数将其合成回屏幕。
从 Android 3.0(API 级别 11)开始,您可以使用 View.setLayerType()
方法更好地控制如何以及何时使用层。此 API 采用两个参数:要使用的层类型和一个可选的 Paint
对象,该对象描述了如何合成层。您可以使用 Paint
参数将颜色滤镜、特殊混合模式或不透明度应用于层。视图可以使用三种层类型之一
LAYER_TYPE_NONE
:视图正常渲染,并且没有屏幕外缓冲区支持。这是默认行为。LAYER_TYPE_HARDWARE
:如果应用程序已启用硬件加速,则视图会在硬件中渲染到硬件纹理中。如果应用程序未启用硬件加速,则此层类型与LAYER_TYPE_SOFTWARE
的行为相同。LAYER_TYPE_SOFTWARE
:视图在软件中渲染到位图中。
您使用的层类型取决于您的目标
- 性能:使用硬件层类型将视图渲染到硬件纹理中。视图渲染到层后,其绘图代码不需要执行,直到视图调用
invalidate()
为止。然后,一些动画(例如 alpha 动画)可以直接应用到层上,这对于 GPU 来说非常高效。 - 视觉效果:使用硬件或软件层类型和
Paint
将特殊视觉处理应用于视图。例如,您可以使用ColorMatrixColorFilter
以黑白方式绘制视图。 - 兼容性:使用软件层类型强制视图在软件中渲染。如果硬件加速的视图(例如,如果您的整个应用程序都已启用硬件加速)遇到渲染问题,这是一种解决硬件渲染管道限制的简单方法。
视图层和动画
当您的应用程序启用硬件加速时,硬件层可以提供更快、更流畅的动画。当动画复杂视图时,这些视图会发出大量绘图操作,因此无法始终以每秒 60 帧的速度运行动画。使用硬件层将视图渲染到硬件纹理可以缓解这种情况。然后,可以使用硬件纹理来动画化视图,从而无需在动画化视图时不断重新绘制视图。除非您更改视图的属性(这会导致调用 invalidate()
),或者您手动调用 invalidate()
,否则不会重新绘制视图。如果您在应用程序中运行动画,但没有获得理想的平滑效果,请考虑在动画视图上启用硬件层。
当视图由硬件层支持时,它的某些属性由层在屏幕上的合成方式处理。设置这些属性将非常高效,因为它们不需要视图失效和重新绘制。以下属性列表会影响层的合成方式。调用任何这些属性的设置器会导致最佳失效,并且不会重新绘制目标视图。
alpha
:更改层的透明度x
、y
、translationX
、translationY
:更改层的位置scaleX
、scaleY
:更改层的大小rotation
、rotationX
、rotationY
:更改层在 3D 空间中的方向pivotX
、pivotY
:更改层的变换原点
这些属性是使用 ObjectAnimator
动画化视图时使用的名称。如果要访问这些属性,请调用相应的设置器或获取器。例如,要修改 alpha 属性,请调用 setAlpha()
。以下代码片段展示了在 3D 中绕 Y 轴旋转视图iew 的最有效方式
Kotlin
view.setLayerType(View.LAYER_TYPE_HARDWARE, null) ObjectAnimator.ofFloat(view, "rotationY", 180f).start()
Java
view.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator.ofFloat(view, "rotationY", 180).start();
由于硬件层会消耗视频内存,因此强烈建议您仅在动画持续时间内启用它们,并在动画完成后禁用它们。您可以使用动画监听器来完成此操作
Kotlin
view.setLayerType(View.LAYER_TYPE_HARDWARE, null) ObjectAnimator.ofFloat(view, "rotationY", 180f).apply { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { view.setLayerType(View.LAYER_TYPE_NONE, null) } }) start() }
Java
view.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { view.setLayerType(View.LAYER_TYPE_NONE, null); } }); animator.start();
有关属性动画的更多信息,请参阅 属性动画。
提示和技巧
切换到硬件加速的 2D 图形可以立即提高性能,但您仍然应该按照以下建议设计应用程序以有效地使用 GPU
- 减少应用程序中的视图数量
- 系统需要绘制的视图越多,速度就越慢。这也适用于软件渲染管道。减少视图是优化 UI 的最简单方法之一。
- 避免过度绘制
- 不要在彼此之上绘制太多层。删除任何完全被顶部的其他不透明视图遮挡的视图。如果您需要在彼此之上绘制多个混合层,请考虑将它们合并为单个层。使用当前硬件的一个好经验法则是每帧不要绘制超过屏幕上像素数量的 2.5 倍(位图中的透明像素算在内!)。
- 不要在绘制方法中创建渲染对象
- 一个常见的错误是每次调用渲染方法时都创建一个新的
Paint
或一个新的Path
。这会强制垃圾收集器更频繁地运行,还会绕过硬件管道中的缓存和优化。 - 不要太频繁地修改形状
- 例如,复杂的形状、路径和圆形是使用纹理蒙版渲染的。每次创建或修改路径时,硬件管道都会创建一个新的蒙版,这可能很昂贵。
- 不要太频繁地修改位图
- 每次更改位图的内容时,它都会在您下次绘制它时作为 GPU 纹理再次上传。
- 谨慎使用 alpha
- 当您使用
setAlpha()
、AlphaAnimation
或ObjectAnimator
使视图半透明时,它会在屏幕外缓冲区中渲染,这会使所需的填充率翻倍。在对非常大的视图应用 alpha 时,请考虑将视图的层类型设置为LAYER_TYPE_HARDWARE
。