硬件加速

从 Android 3.0(API 级别 11)开始,Android 2D 渲染管道支持硬件加速,也就是说,在 View 的画布上执行的所有绘制操作都会使用 GPU。启用硬件加速需要更多资源,因此应用会占用更多内存。

如果您的目标 API 级别为 14 及更高级别,则硬件加速默认处于启用状态,但也可以明确启用该功能。如果您的应用仅使用标准视图和 Drawable,则全局启用硬件加速不会造成任何不良绘制效果。不过,并非所有 2D 绘制操作都支持硬件加速,因此启用硬件加速可能会影响您的部分自定义视图或绘制调用。具体问题通常以不可见的元素、异常或错误渲染的像素显现。为了解决此问题,Android 允许您在多个级别选择是启用还是停用硬件加速。请参阅控制硬件加速

如果您的应用执行自定义绘制,请在启用硬件加速的实际硬件设备上测试应用,以检查是否存在任何问题。不受支持的绘制操作部分介绍了已知硬件加速问题和相应的解决方案。

另请参阅通过框架 API 支持 OpenGLRenderscript

控制硬件加速

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

  • 应用
  • Activity
  • 窗口
  • 视图

应用级别

在 Android 清单文件中,将以下属性添加到 <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()。如果某个视图已附加到硬件加速窗口,则仍可以使用未经过硬件加速的画布进行绘制。例如,将视图绘制为位图以进行缓存就会发生这种情况。

Android 绘制模型

启用硬件加速后,Android 框架会采用新的绘制模型,该模型利用显示列表将您的应用渲染到屏幕上。要充分了解显示列表以及它们可能如何影响您的应用,最好再了解一下 Android 如何在不启用硬件加速的情况下绘制视图。下面几部分介绍了基于软件的绘制模型和硬件加速绘制模型。

基于软件的绘制模型

在软件绘制模型中,绘制视图分为以下两步:

  1. 对层次结构进行无效化处理
  2. 绘制层次结构

每当应用需要更新其界面的一部分时,就会对内容已发生更改的所有视图调用 invalidate()(或其变体之一)。无效化消息会一直传播到视图层次结构上层,以计算需要重新绘制的屏幕区域(脏区域)。然后,Android 系统会绘制层次结构中与脏区域交互的所有视图。遗憾的是,这种绘制模型具有以下两个缺点:

  • 第一,每次绘制时该模型都需要执行大量代码。例如,如果您的应用对某个按钮调用 invalidate() 且该按钮位于另一个视图上方,那么即使该视图未发生更改,Android 系统仍会重新绘制该视图。
  • 第二,该绘制模型会隐藏应用中的错误。由于 Android 系统会在视图与脏区域交互时重新绘制视图,因此系统可能会重新绘制内容发生更改的视图,即使未对其调用 invalidate() 也是如此。如果发生这种情况,您要依赖其他经过无效化处理的视图才能获得正确的行为。每次修改应用时,此行为都可能会发生更改。因此,每次修改会影响视图绘制代码的数据或状态后,您都要对自定义视图调用 invalidate()

注意:Android 视图会在其属性(例如 TextView 中的背景颜色或文本)发生更改时自动调用 invalidate()

硬件加速绘制模型

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

  1. 对层次结构进行无效化处理
  2. 记录并更新显示列表
  3. 绘制显示列表

使用此模型时,您无法依赖与脏区域交互的视图来执行其 draw() 方法。要确保 Android 系统会记录视图的显示列表,您必须调用 invalidate()。如果忘记执行此操作,则视图在发生更改后看起来仍然没有变化。

使用显示列表还有助于改进动画性能,因为设置特定属性(例如 Alpha 或旋转)不需要对目标视图进行无效化处理(该操作是自动完成的)。这项优化还适用于具有显示列表的视图(如果应用经过硬件加速,则适用于所有视图)。例如,假设有一个 LinearLayout,其中包含一个ListView(位于 Button 之上)。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()
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

画布缩放

硬件加速 2D 渲染管道最初是为了支持不可缩放的绘制构建的,其中一些绘制操作会以较高的缩放值显著降低质量。这些操作实现为按 1.0 的缩放值绘制的纹理,由 GPU 进行转换。从 API 级别 28 开始,所有绘制操作都可以顺利缩放。

下表列出了何时更改实现以正确处理大规模缩放:
要缩放的绘制操作 第一个支持的 API 级别
drawText() 18
drawPosText() 28
drawTextOnPath() 28
简单的形状* 17
复杂的形状* 28
drawPath() 28
阴影层 28

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

如果缺失以上任意功能或限制会对您的应用造成影响,您可以调用 setLayerType(View.LAYER_TYPE_SOFTWARE, null),仅针对应用中受影响的部分关闭硬件加速。这样一来,您仍能针对其余任何部分利用硬件加速。如需详细了解如何在应用中的不同级别启用和停用硬件加速,请参阅控制硬件加速

视图层

在所有 Android 版本中,视图能够通过以下两种方式渲染到屏幕外缓冲区:使用视图的绘制缓存或使用 Canvas.saveLayer()。屏幕外缓冲区或层具有多种用途。在为复杂的视图添加动画效果或应用合成效果时,您可以使用屏幕外缓冲区或层获得更好的效果。例如,您可以使用 Canvas.saveLayer() 实现淡入淡出效果,以将视图暂时渲染到层,然后使用不透明度系数将其合成回屏幕上。

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

要使用何种层类型取决于您的目标:

  • 性能:使用硬件层类型可将视图渲染为硬件纹理。将视图渲染为层后,在该视图调用 invalidate() 之前,无需执行其绘制代码。然后,可将 Alpha 动画等部分动画直接应用到层,GPU 可非常高效地完成此操作。
  • 视觉效果:使用硬件层或软件层类型和 Paint 可将特殊视觉处理应用到视图。例如,您可以使用 ColorMatrixColorFilter 绘制黑白视图。
  • 兼容性:使用软件层类型可强制在软件中渲染视图。如果经过硬件加速的视图(例如,如果整个应用都经过硬件加速)遇到渲染问题,采用这种方法可轻松解决硬件渲染管道的局限性。

视图层和动画

如果应用经过硬件加速,硬件层能够提供更快且更顺畅的动画。在为需要发出大量绘制操作的复杂视图添加动画效果时,以 60 帧/秒的速度运行动画并非总能实现。使用硬件层将视图渲染为硬件纹理可在一定程度上解决此问题。然后,硬件纹理可用于为视图添加动画效果,这样视图在动画效果添加过程中无需不断自行重新绘制。除非您更改视图的属性(这会调用 invalidate()),或者手动调用 invalidate(),否则系统不会重新绘制视图。如果您在应用中运行动画,但没有获得想要的顺畅动画,请考虑对添加动画效果之后的视图启用硬件层。

如果视图由硬件层提供支持,则其部分属性可通过在屏幕上合成层的方式处理。设置这些属性有助于提高效率,因为它们不需要先对视图进行无效化处理后再重新绘制。下面列出的属性会影响层的合成方式。针对以下任何属性调用 setter 方法会得到最佳无效化效果,且无需重新绘制目标视图:

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

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

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:

减少应用中的视图数量
系统需要绘制的视图越多,运行速度越慢。这也适用于软件渲染管道。减少视图是优化界面最简单的方法之一。
避免过度绘制
请勿在彼此上方绘制过多层。移除所有被上方的其他不透明视图完全遮挡的视图。如果您需要在彼此上方混合绘制多个层,请考虑将它们合并为一个层。对于目前的硬件来说,绘制的层数最好不超过屏幕上每帧像素数的 2.5 倍(透明像素,以位图计数!)。
请勿在绘制方法中创建渲染对象
一个常见的错误是,每次调用渲染方法时都创建新的 PaintPath。这会强制垃圾回收器更频繁地运行,同时还会绕过硬件管道中的缓存和优化。
请勿过于频繁地修改形状
例如,使用纹理遮罩渲染复杂的形状、路径和圆圈。每次创建或修改路径时,硬件管道都会创建新的遮罩,成本可能比较高。
请勿过于频繁地修改位图
每次更改位图的内容时,系统都会在您下次绘制时将其作为 GPU 纹理再次上传。
谨慎使用 Alpha
当您使用 setAlpha()AlphaAnimationObjectAnimator 将视图设置为半透明时,该视图会在屏幕外缓冲区渲染,导致所需的填充率翻倍。在超大视图上应用 Alpha 时,请考虑将视图的层类型设置为 LAYER_TYPE_HARDWARE