硬件加速

从 Android 3.0(API 级别 11)开始,Android 2D 渲染管线支持硬件加速,这意味着在 View 画布上执行的所有绘制操作都会使用 GPU。由于启用硬件加速需要更多资源,您的应用会消耗更多 RAM。

如果您的目标 API 级别 >= 14,则默认启用硬件加速,也可以显式启用。如果您的应用仅使用标准视图和 Drawable,则全局开启不应导致任何不良绘制效果。但是,由于并非所有 2D 绘制操作都支持硬件加速,因此开启它可能会影响您的某些自定义视图或绘制调用。问题通常表现为不可见元素、异常或像素渲染错误。为了解决此问题,Android 提供了在多个级别启用或禁用硬件加速的选项。请参阅控制硬件加速

如果您的应用执行自定义绘制,请在开启硬件加速的实际硬件设备上测试您的应用,以发现任何问题。支持的绘制操作部分介绍了硬件加速的已知问题以及如何解决这些问题。

另请参阅使用框架 API 的 OpenGLRenderscript

控制硬件加速

您可以在以下级别控制硬件加速

  • 应用
  • Activity
  • 窗口
  • 视图

应用级别

在您的 Android Manifest 文件中,将以下属性添加到 <application> 标签以在整个应用中启用硬件加速

<application android:hardwareAccelerated="true" ...>

Activity 级别

如果您的应用在全局开启硬件加速后行为异常,您也可以针对单个 Activity 进行控制。要在 Activity 级别启用或禁用硬件加速,可以使用 <activity> 元素的 android:hardwareAccelerated 属性。以下示例为整个应用启用了硬件加速,但为一个 Activity 禁用了它

<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);

注意:目前您无法在视图级别启用硬件加速。视图层除了禁用硬件加速之外还有其他功能。有关其用途的更多信息,请参阅视图层

确定视图是否已进行硬件加速

有时,了解应用当前是否已进行硬件加速会很有用,尤其是对于自定义视图等情况。如果您的应用执行大量自定义绘制且并非所有操作都得到新渲染管线的正确支持,这一点尤其有用。

有两种不同的方式可以检查应用是否已进行硬件加速

如果您必须在绘制代码中执行此检查,请尽可能使用 Canvas.isHardwareAccelerated(),而不是 View.isHardwareAccelerated()。当视图附加到已进行硬件加速的窗口时,仍然可以使用未进行硬件加速的 Canvas 进行绘制。例如,在为了缓存目的将视图绘制到位图时会发生这种情况。

Android 绘制模型

启用硬件加速后,Android 框架会使用新的绘制模型,该模型利用显示列表将您的应用渲染到屏幕。为了充分理解显示列表及其如何影响您的应用,了解 Android 在未进行硬件加速的情况下如何绘制视图也很有用。以下部分描述了基于软件和硬件加速的绘制模型。

基于软件的绘制模型

在软件绘制模型中,视图通过以下两个步骤绘制

  1. 使层次结构失效
  2. 绘制层次结构

每当应用需要更新其 UI 的一部分时,都会在其内容已更改的任何视图上调用 invalidate()(或其变体之一)。无效消息会一直传播到视图层次结构的顶部,以计算需要重绘的屏幕区域(脏区域)。然后,Android 系统会绘制层次结构中与脏区域相交的任何视图。遗憾的是,这种绘制模型有两个缺点

  • 首先,这种模型在每次绘制传递时都需要执行大量代码。例如,如果您的应用在按钮上调用 invalidate(),并且该按钮位于另一个视图之上,即使该视图没有改变,Android 系统也会重新绘制该视图。
  • 第二个问题是,这种绘制模型可能会隐藏应用中的 bug。由于 Android 系统会在视图与脏区域相交时重新绘制视图,因此即使未在其上调用 invalidate(),您更改内容的视图也可能被重绘。发生这种情况时,您依赖于另一个视图失效来获得正确的行为。每次修改应用时,这种行为都可能发生变化。因此,每当您修改影响视图绘制代码的数据或状态时,都应始终在自定义视图上调用 invalidate()

注意:当 Android 视图的属性更改时,例如背景颜色或 TextView 中的文本,它们会自动调用 invalidate()

硬件加速绘制模型

Android 系统仍然使用 invalidate()draw() 来请求屏幕更新和渲染视图,但处理实际绘制的方式不同。Android 系统不会立即执行绘制命令,而是将其记录在显示列表中,显示列表包含视图层次结构绘制代码的输出。另一个优化是,Android 系统只需为通过 invalidate() 调用标记为脏的视图记录和更新显示列表。未失效的视图只需重新发出先前记录的显示列表即可重绘。新的绘制模型包含三个阶段

  1. 使层次结构失效
  2. 记录和更新显示列表
  3. 绘制显示列表

在此模型下,您不能依赖视图与脏区域相交来执行其 draw() 方法。要确保 Android 系统记录视图的显示列表,必须调用 invalidate()。忘记这样做会导致视图在更改后仍然看起来相同。

使用显示列表也有利于动画性能,因为设置特定属性(例如 Alpha 或旋转)不需要使目标视图失效(这是自动完成的)。此优化也适用于具有显示列表的视图(当您的应用进行硬件加速时,任何视图都具有此特性)。例如,假设有一个 LinearLayout,其中包含一个位于 Button 上方的 ListViewLinearLayout 的显示列表如下所示

  • 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 (framebuffer) 28
PorterDuff.Mode.LIGHTEN (framebuffer) 28
PorterDuff.Mode.OVERLAY (framebuffer) 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

注意:“简单”形状是指在使用没有 PathEffect 且不包含非默认连接(通过 setStrokeJoin() / setStrokeMiter())的 Paint 发出的 drawRect()drawCircle()drawOval()drawRoundRect()drawArc()(使用 useCenter=false)命令。这些绘制命令的其他实例在上表中属于“复杂”。

如果您的应用受到这些缺失功能或限制中的任何一个影响,您可以仅针对受影响的应用部分关闭硬件加速,方法是调用 setLayerType(View.LAYER_TYPE_SOFTWARE, null)。这样,您仍然可以在其他地方利用硬件加速。有关如何在应用的不同级别启用和禁用硬件加速的更多信息,请参阅控制硬件加速

视图层

在所有版本的 Android 中,视图都可以渲染到离屏缓冲区,可以通过视图的绘制缓存或使用 Canvas.saveLayer() 来实现。离屏缓冲区或层有多种用途。您可以使用它们在为复杂视图添加动画时获得更好的性能,或应用合成效果。例如,您可以使用 Canvas.saveLayer() 将视图临时渲染到层中,然后以不透明度因子将其合成回屏幕来实现淡入淡出效果。

从 Android 3.0(API 级别 11)开始,您可以使用 View.setLayerType() 方法更好地控制如何以及何时使用层。此 API 接受两个参数:您要使用的层类型和一个可选的 Paint 对象,该对象描述层应如何合成。您可以使用 Paint 参数为层应用颜色滤镜、特殊混合模式或不透明度。视图可以使用以下三种层类型之一

您使用的层类型取决于您的目标

  • 性能:使用硬件层类型将视图渲染到硬件纹理中。一旦视图渲染到层中,其绘制代码就不必执行,直到视图调用 invalidate()。然后可以将一些动画(例如 Alpha 动画)直接应用于层上,这对于 GPU 来说非常高效。
  • 视觉效果:使用硬件或软件层类型以及 Paint 为视图应用特殊的视觉处理。例如,您可以使用 ColorMatrixColorFilter 绘制黑白视图。
  • 兼容性:使用软件层类型强制视图在软件中渲染。如果已进行硬件加速的视图(例如,如果您的整个应用已进行硬件加速)出现渲染问题,这是绕过硬件渲染管线限制的一种简单方法。

视图层和动画

硬件层可以在您的应用进行硬件加速时提供更快、更流畅的动画。当为发出大量绘制操作的复杂视图添加动画时,以每秒 60 帧的速度运行动画并非总是可行。可以通过使用硬件层将视图渲染到硬件纹理来缓解此问题。然后可以使用硬件纹理为视图添加动画,从而无需在动画播放时不断重新绘制视图。除非您更改视图的属性(这将调用 invalidate())或手动调用 invalidate(),否则不会重新绘制视图。如果您在应用中运行动画但未获得所需的流畅效果,请考虑在动画视图上启用硬件层。

当视图由硬件层支持时,其某些属性的处理方式由层在屏幕上的合成方式决定。设置这些属性将非常高效,因为它们不需要使视图失效并重新绘制。以下属性列表会影响层的合成方式。调用这些属性中任何一个的 setter 会导致最佳的失效,并且不会重新绘制目标视图

  • alpha:更改层的不透明度
  • xytranslationXtranslationY:更改层的位置
  • scaleXscaleY:更改层的大小
  • rotationrotationXrotationY:更改层在 3D 空间中的方向
  • pivotXpivotY:更改层的变换原点

这些属性是使用 ObjectAnimator 为视图添加动画时使用的名称。如果要访问这些属性,请调用相应的 setter 或 getter。例如,要修改 alpha 属性,请调用 setAlpha()。以下代码片段展示了围绕 Y 轴在 3D 空间中旋转视图的最有效方法

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()AlphaAnimationObjectAnimator 使视图半透明时,它会在离屏缓冲区中渲染,这会使所需的填充率加倍。在非常大的视图上应用 Alpha 时,请考虑将视图的层类型设置为 LAYER_TYPE_HARDWARE