Android 2D 渲染管道支持硬件加速,也就是说,在画布上执行的所有绘制操作都会使用 GPU。启用硬件加速需要更多资源,因此应用会占用更多 RAM。
硬件加速默认处于启用状态。如果您的应用仅使用标准可组合项,则全局启用硬件加速不会造成任何不良绘制效果。不过,并非所有 2D 绘制操作都支持硬件加速,因此启用硬件加速可能会影响您的部分自定义绘制调用。具体问题通常以不可见的元素、异常或错误渲染的像素显现。为了解决此问题,Android 允许您在多个级别选择是启用还是停用硬件加速。请参阅控制硬件加速。
如果您的应用执行自定义绘制,请在启用硬件加速的实际硬件设备上测试应用,以检查是否存在任何问题。支持绘制操作部分介绍了已知硬件加速问题和相应的解决方案。
另请参阅通过框架 API 支持 OpenGL。
控制硬件加速
您可以在以下级别控制硬件加速:
- 应用
- activity
- 窗口
- 可组合项
应用级别
在 Android 清单文件中,将以下属性添加到 <application> 标记中,为整个应用启用硬件加速:
<application android:hardwareAccelerated="true" ...>
activity 级别
如果您的应用在全局启用硬件加速时无法正常运行,您也可以针对个别 activity 启用或停用硬件加速。如需在 activity 级别启用或停用硬件加速,您可以使用 <activity> 元素的 android:hardwareAccelerated 属性。以下示例展示了如何为整个应用启用硬件加速,但为一个 activity 停用硬件加速:
<application android:hardwareAccelerated="true">
<activity ... />
<activity android:hardwareAccelerated="false" />
</application>
窗口级别
如果您需要实现更精细的控制,可以使用以下代码为给定窗口启用硬件加速:
window.setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
)
可组合项级别
在 Compose 中,没有可用于停用硬件加速的每个可组合项开关。
如需将可组合项渲染到自己的层中,请使用 Modifier.graphicsLayer。这样一来,转换属性(例如 alpha、scaleX、scaleY、translationX、translationY、rotationX、rotationY、rotationZ 和 transformOrigin)就可以在不重新运行可组合函数的绘制代码的情况下发生变化。为获得最佳性能,请始终使用修饰符的 lambda 形式来设置这些属性。
如需明确强制使用屏幕外缓冲区以进行高级绘制操作(例如图层内的自定义混合),请使用 CompositingStrategy.Offscreen。如需了解详情,请参阅图形修饰符。
如果您有严格要求使用软件渲染的自定义绘制操作,可以使用 AndroidView 托管旧版 View,并对该视图调用 setLayerType(View.LAYER_TYPE_SOFTWARE, null)。
支持绘制操作
经过硬件加速后,2D 渲染管道支持最常用的 Canvas 绘制操作以及很多不太常用的操作。用于渲染 Android 系统内置应用、标准可组合项以及常见的高级视觉效果(例如反射和平铺纹理)的所有绘制操作均受到支持。
下表介绍了各种操作在各个 API 级别的支持级别:
| 第一个支持的 API 级别 | ||||
| 画布 | ||||
| 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 | |||
| 绘制 | ||||
| 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 | |||
| 着色器 | ||||
| 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 |
如果您依赖的绘制操作未进行硬件加速,请将受影响的绘制渲染到屏幕外软件 Bitmap(或 ImageBitmap)中,然后绘制结果。其余界面会保留硬件加速路径。
提示和技巧
切换到硬件加速的 2D 图形可立即提升性能,但您仍应按照以下建议设计应用,以便有效利用 GPU:
- 最大限度地降低布局复杂性和重新组合
- 保持布局树较浅,并限制重新组合的次数。将状态读取推迟到尽可能小的范围内,以便更改仅重绘尽可能小的区域。例如,在
Modifier.graphicsLayer { }内读取动画状态,而不是在可组合项的正文中读取。如需了解详情,请参阅 Jetpack Compose 性能。 - 避免过度绘制
- 请勿在彼此上方绘制过多层。移除所有被上方的其他不透明元素完全遮挡的界面元素。如果您需要在彼此上方混合绘制多个层,请考虑将它们合并为一个层。对于目前的硬件来说,绘制的层数最好不超过屏幕上每帧像素数的 2.5 倍(透明像素,以位图计数!)。
- 请勿在绘制方法中创建渲染对象
- 一个常见的错误是,每次调用渲染方法时都创建新的
Paint或Path。这会强制垃圾回收器更频繁地运行,同时还会绕过硬件管道中的缓存和优化。为避免这种情况,请重用并更改对象:- 使用标准方法:标准
DrawScope方法(如drawRect和drawCircle)已在内部重复使用Paint对象,无需开发者分配。 - 进行突变而非重新分配:在编写自定义逻辑时,请使用
path.rewind清除现有Path,而不是实例化新的Path。 - 高效保持状态:在可组合函数内,使用
remember { Path() }分配对象一次。如果您要构建可重用的自定义修饰符扩展程序,请使用DrawModifierNode实现自定义Modifier.Node,以分配和重用对象,而不会导致新的堆分配。
- 使用标准方法:标准
- 请勿过于频繁地修改形状
- 例如,使用纹理遮罩渲染复杂的形状、路径和圆圈。每次创建或修改路径时,硬件流水线都会创建新的遮罩,成本可能比较高。
- 请勿过于频繁地修改位图
- 每次更改位图的内容时,系统都会在您下次绘制时将其作为 GPU 纹理再次上传。
- 谨慎使用 Alpha
- 当您使用
Modifier.alpha或 Compose 动画 API 将可组合项设置为半透明时,该可组合项通常会在屏幕外缓冲区渲染,导致所需的填充率翻倍。为避免非重叠内容的屏幕外缓冲区开销,请设置CompositingStrategy.ModulateAlpha。对于单个绘制调用,请直接将 Alpha 应用于绘制命令(例如使用color = Color.Red.copy(alpha = 0.5f)),而无需创建图层。