硬件加速

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

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

如果您的应用程序执行自定义绘制,请在启用了硬件加速的实际硬件设备上测试您的应用程序,以查找任何问题。 对绘制操作的支持 部分描述了与硬件加速相关的已知问题以及如何解决这些问题。

另请参阅 使用框架 API 的 OpenGLRenderscript

控制硬件加速

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

  • 应用程序
  • 活动
  • 窗口
  • 视图

应用程序级别

在您的 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);

注意:目前無法在檢視層級啟用硬體加速。檢視層除了停用硬體加速外,還有其他功能。如需其用途的更多資訊,請參閱 檢視層

確定檢視是否已硬體加速

應用程式有時需要知道它是否已硬體加速,特別是針對自訂檢視等情況。如果您的應用程式執行大量自訂繪製,而且並非所有操作都由新的渲染管線適當支援,這將特別有用。

有兩種不同的方法可以檢查應用程式是否已硬體加速。

如果您必須在繪製程式碼中進行此檢查,請盡可能使用 Canvas.isHardwareAccelerated() 而不是 View.isHardwareAccelerated()。當檢視附加到硬體加速視窗時,它仍然可以使用非硬體加速 Canvas 繪製。例如,當將檢視繪製到位元圖以進行快取時,就會發生這種情況。

Android 繪製模型

啟用硬體加速後,Android 架構會使用新的繪製模型,該模型利用顯示清單將應用程式渲染到螢幕上。要完全理解顯示清單及其對應用程式的影響,了解 Android 在沒有硬體加速的情況下如何繪製檢視也很有用。以下各節描述軟體和硬體加速繪製模型。

軟體繪製模型

在軟體繪製模型中,檢視使用以下兩個步驟繪製。

  1. 使層級無效
  2. 繪製層級

每當應用程式需要更新 UI 的一部分時,它都會在內容已變更的任何檢視上呼叫 invalidate()(或其變體之一)。無效訊息會一直傳播到檢視層級的頂部,以計算需要重新繪製的螢幕區域(髒區域)。然後,Android 系統會繪製與髒區域相交的層級中的任何檢視。不幸的是,此繪製模型有兩個缺點。

  • 首先,此模型需要在每次繪製傳遞時執行大量程式碼。例如,如果您的應用程式在按鈕上呼叫 invalidate(),而該按鈕位於另一個檢視的頂部,即使該檢視沒有變更,Android 系統也會重新繪製該檢視。
  • 第二個問題是繪製模型可能會隱藏應用程式中的錯誤。由於 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(幀緩衝器) 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())。上述圖表中,這些 draw 命令的其他實例屬於 'Complex'。

如果您的應用程式受到任何這些缺失的功能或限制的影響,您可以通過呼叫 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(),否则视图不会重新绘制。如果您在应用程序中运行动画但没有获得所需的平滑结果,请考虑在动画视图上启用硬件图层。

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

  • alpha: 更改图层的透明度
  • x, y, translationX, translationY: 更改图层的位置
  • scaleX, scaleY: 更改图层的大小
  • rotation, rotationX, rotationY: 更改图层在 3D 空间中的方向
  • pivotX, pivotY: 更改图层的变换原点

这些属性是使用 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

减少应用程序中的视图数量
系统需要绘制的视图越多,速度就越慢。这同样适用于软件渲染管道。减少视图是优化 UI 的最简单方法之一。
避免过度绘制
不要在彼此之上绘制太多层。删除任何被其上方的其他不透明视图完全遮挡的视图。如果您需要绘制几个彼此混合的层,请考虑将它们合并成单个层。对于当前的硬件,一个好的经验法则是每帧不要绘制超过屏幕像素数量的 2.5 倍(位图中的透明像素算数!)。
不要在 draw 方法中创建渲染对象
一个常见的错误是在每次调用渲染方法时创建一个新的 Paint 或新的 Path。这会强制垃圾收集器更频繁地运行,还会绕过硬件管道中的缓存和优化。
不要太频繁地修改形状
例如,复杂的形状、路径和圆圈是使用纹理遮罩渲染的。每次创建或修改路径时,硬件管道都会创建一个新的遮罩,这可能很昂贵。
不要太频繁地修改位图
每次更改位图内容时,都会在下次绘制时将其再次上传为 GPU 纹理。
谨慎使用 alpha
当您使用 setAlpha()AlphaAnimationObjectAnimator 使视图半透明时,它将在屏幕外缓冲区中渲染,这会使所需的填充率翻倍。在对非常大的视图应用 alpha 时,请考虑将视图的图层类型设置为 LAYER_TYPE_HARDWARE