RenderScript 入門

RenderScript 是一個允許在 Android 上進行高效能平行計算的框架。你編寫的指令碼將並行執行所有可用處理器(例如 CPU,GPU 等),使你可以專注於要實現的任務,而不是如何安排和執行。

指令碼是用基於 C99 的語言編寫的(C99 是 C 程式語言標準的舊版本)。對於每個指令碼,都會建立一個 Java 類,使你可以輕鬆地在 Java 程式碼中與 RenderScript 進行互動。

設定專案

使用 Android Framework 庫或支援庫,有兩種不同的方法可以訪問應用程式中的 RenderScript。即使你不希望在 API 級別 11 之前定位裝置,也應始終使用支援庫實施,因為它可確保裝置在許多不同裝置上的相容性。要使用支援庫實現,你至少需要使用構建工具版本 18.1.0

現在讓我們設定應用程式的 build.gradle 檔案:

android {
    compileSdkVersion 24
    buildToolsVersion '24.0.1'

    defaultConfig {
        minSdkVersion 8
        targetSdkVersion 24

        renderscriptTargetApi 18
        renderscriptSupportModeEnabled true
    }
}
  • renderscriptTargetApi:應該設定為最早的 API 級別,它提供你需要的所有 RenderScript 功能。
  • renderscriptSupportModeEnabled:這使得支援庫 RenderScript 實現的使用成為可能。

RenderScript 的工作原理

典型的 RenderScript 包含兩件事:核心和函式。一個函式就是它聽起來的樣子 - 它接受一個輸入,用該輸入做一些事情並返回一個輸出。核心是 RenderScript 的真正力量所在。

核心是針對 Allocation 內的每個元素執行的函式。Allocation 可用於將 Bitmapbyte 陣列之類的資料傳遞給 RenderScript,它們也可用於從核心獲取結果。核心可以將一個 Allocation 作為輸入,另一個作為輸出,或者它們可以僅修改一個 Allocation 內的資料。

你可以編寫一個核心,但也有許多預定義的核心可用於執行常見操作,如高斯影象模糊。

如前所述,每個 RenderScript 檔案都會生成一個類來與之互動。這些類始終以字首 ScriptC_ 開頭,後跟 RenderScript 檔案的名稱。例如,如果你的 RenderScript 檔名為 example,那麼生成的 Java 類將被稱為 ScriptC_example。所有預定義的指令碼都以字首 Script 開頭 - 例如高斯影象模糊指令碼稱為 ScriptIntrinsicBlur

編寫第一個 RenderScript

以下示例基於 GitHub 上的示例。它通過修改影象的飽和度來執行基本影象處理。你可以在這裡找到原始碼,如果你想自己玩它,請檢視它。這是結果應該是什麼樣的快速 gif:

演示圖片

RenderScript Boilerplate

RenderScript 檔案位於專案的 src/main/rs 資料夾中。每個檔案的副檔名為 .rs,頂部必須包含兩個 #pragma 語句:

#pragma version(1)
#pragma rs java_package_name(your.package.name)
  • #pragma version(1):這可用於設定你正在使用的 RenderScript 版本。目前只有版本 1。

  • #pragma rs java_package_name(your.package.name):這可用於設定生成的 Java 類的包名,以與此特定 RenderScript 進行互動。

你通常應在每個 RenderScript 檔案中設定另一個 #pragma,它用於設定浮點精度。你可以將浮點精度設定為三個不同的級別:

  • #pragma rs_fp_full:這是具有最高精度的最嚴格設定,如果不指定任何內容,它也是預設值。如果需要高浮點精度,則應使用此方法。
  • #pragma rs_fp_relaxed:這確保了不高的浮點精度,但在某些體系結構上,它可以實現一系列優化,這些優化可以使指令碼執行得更快。
  • #pragma rs_fp_imprecise:這確保了更低的精度,如果浮點精度對於你的指令碼並不重要,則應該使用它。

大多數指令碼只能使用 #pragma rs_fp_relaxed,除非你真的需要高浮點精度。

全域性變數

現在就像在 C 程式碼中一樣,你可以定義全域性變數或常量:

const static float3 gMonoMult = {0.299f, 0.587f, 0.114f};

float saturationLevel = 0.0f;

變數 gMonoMult 的型別為 float3。這意味著它是一個由 3 個浮點陣列成的向量。另一個名為 saturationValuefloat 變數不是常量,因此你可以在執行時將其設定為你喜歡的值。你可以在核心或函式中使用這樣的變數,因此它們是另一種為 RenderScripts 提供輸入或接收輸出的方法。對於每個非常量變數,將在關聯的 Java 類上生成 getter 和 setter 方法。

但現在讓我們開始實現核心。出於本示例的目的,我不打算解釋核心中用於修改影象飽和度的數學,而是將重點放在如何實現核心以及如何使用它。在本章的最後,我將快速解釋這個核心中的程式碼實際上在做什麼。

核心一般

我們先來看看原始碼:

uchar4 __attribute__((kernel)) saturation(uchar4 in) {
    float4 f4 = rsUnpackColor8888(in);
    float3 dotVector = dot(f4.rgb, gMonoMult);
    float3 newColor = mix(dotVector, f4.rgb, saturationLevel);
    return rsPackColorTo8888(newColor);
}

正如你所看到的,它看起來像普通的 C 函式,但有一個例外:返回型別和方法名稱之間的 __attribute__((kernel))。這就是告訴 RenderScript 這個方法是一個核心。你可能會注意到的另一件事是此方法接受 uchar4 引數並返回另一個 uchar4 值。uchar4 就像我們之前在章節中討論的 float3 變數一樣 - 向量。它包含 4 個 uchar 值,它們只是 0 到 255 範圍內的位元組值。

你可以通過多種不同方式訪問這些單獨的值,例如 in.r 將返回與畫素的紅色通道對應的位元組。我們使用 uchar4,因為每個畫素由 4 個值組成 - r 表示紅色,g 表示綠色,b 表示藍色,a 表示 alpha - 你可以用這個速記來訪問它們。RenderScript 還允許你從向量中獲取任意數量的值,並使用它們建立另一個向量。例如,in.rgb 將返回一個 uchar3 值,該值僅包含沒有 alpha 值的畫素的紅色,綠色和藍色部分。

在執行時,RenderScript 將為影象的每個畫素呼叫此 Kernel 方法,這就是返回值和引數只是一個 uchar4 值的原因。RenderScript 將在所有可用處理器上並行執行許多這些呼叫,這就是 RenderScript 如此強大的原因。這也意味著你不必擔心執行緒或執行緒安全,你可以只為每個畫素實現你想要做的任何事情,RenderScript 負責其餘部分。

在 Java 中呼叫核心時,你提供兩個 Allocation 變數,一個包含輸入資料,另一個包含接收輸出的變數。將為輸入 Allocation 中的每個值呼叫你的 Kernel 方法,並將結果寫入輸出 Allocation

RenderScript Runtime API 方法

在上面的核心中,使用了一些開箱即用的方法。RenderScript 提供了許多這樣的方法,它們對於你將要使用 RenderScript 進行的幾乎任何操作都至關重要。其中包括進行數學運算的方法,如 sin() 和輔助方法,如 mix(),它根據另一個值混合兩個值。但是在處理向量,四元數和矩陣時,還有一些方法可以進行更復雜的操作。

如果你想了解有關特定方法的更多資訊或者正在尋找執行常規操作(如計算矩陣的點積)的特定方法,則官方 RenderScript 執行時 API 參考 是最佳資源。你可以在此處找到此文件。

核心實現

現在讓我們來看看這個核心正在做什麼的具體細節。這是核心中的第一行:

float4 f4 = rsUnpackColor8888(in);

第一行呼叫內建方法 rsUnpackColor8888() ,它將 uchar4 值轉換為 float4 值。每個顏色通道也被轉換到 0.0f - 1.0f 的範圍,其中 0.0f 對應於 01.0f255 的位元組值。這樣做的主要目的是使這個核心中的所有數學運算更加簡單。

float3 dotVector = dot(f4.rgb, gMonoMult);

下一行使用內建方法 dot() 計算兩個向量的點積。gMonoMult 是一個常數值,我們定義了上面幾章。由於兩個向量需要具有相同的長度來計算點積,並且因為我們只想影響顏色通道而不是畫素的 alpha 通道,所以我們使用簡寫 .rgb 來獲得一個新的 float3 向量,它只包含紅色,綠色和藍色通道。我們這些仍然記得學校如何使用點積的人會很快注意到點積應該只返回一個值而不是向量。然而在上面的程式碼中,我們將結果分配給 float3 向量。這也是 RenderScript 的一個功能。為向量指定一維數時,向量中的所有元素都將設定為該值。

float3 example = 2.0f;

因此,上面的點積的結果被分配給上面的 float3 向量中的每個元素。

現在我們實際使用全域性變數 saturationLevel 修改影象飽和度的部分:

float3 newColor = mix(dotVector, f4.rgb, saturationLevel);

這使用內建方法 mix() 將原始顏色與我們在上面建立的點積向量混合在一起。它們如何混合在一起取決於全域性 saturationLevel 變數。因此,0.0fsaturationLevel 將使得到的顏色不具有原始顏色值的一部分,並且僅由 dotVector 中的值組成,這導致黑白或灰色影象。1.0f 的值將導致產生的顏色完全由原始顏色值組成,而 1.0f 上方的值將使原始顏色相乘,使其更加明亮和強烈。

return rsPackColorTo8888(newColor);

這是核心的最後一部分。 rsPackColorTo8888()float3 向量轉換回 uchar4 值,然後返回該值。結果位元組值被鉗制到 0 到 25​​5 之間的範圍,因此高於 1.0f 的浮點值將導致位元組值為 255,低於 0.0 的值將導致位元組值 0

這就是整個核心實現。現在只剩下一部分:如何在 Java 中呼叫核心。

用 Java 呼叫 RenderScript

基本

正如上面針對每個 RenderScript 檔案所提到的,生成了一個 Java 類,它允許你與指令碼進行互動。這些檔案的字首為 ScriptC_,後跟 RenderScript 檔案的名稱。要建立這些類的例項,首先需要 RenderScript 類的例項:

final RenderScript renderScript = RenderScript.create(context);

靜態方法 create() 可用於從 Context 建立 RenderScript 例項。然後,你可以例項化為指令碼生成的 Java 類。如果你呼叫了 RenderScript 檔案 saturation.rs,那麼該類將被稱為 ScriptC_saturation

final ScriptC_saturation script = new ScriptC_saturation(renderScript);

在這個類上,你現在可以設定飽和度並呼叫核心。為 saturationLevel 變數生成的 setter 將具有字首 set_,後跟變數的名稱:

script.set_saturationLevel(1.0f);

還有一個以 get_ 為字首的 getter,可以讓你獲得當前設定的飽和度:

float saturationLevel = script.get_saturationLevel();

你在 RenderScript 中定義的核心以 forEach_ 為字首,後跟核心方法的名稱。我們編寫的核心需要輸入 Allocation 和輸出 Allocation 作為其引數:

script.forEach_saturation(inputAllocation, outputAllocation);

輸入 Allocation 需要包含輸入影象,並且在 forEach_saturation 方法完成後,輸出分配將包含修改的影象資料。

一旦你有了 Allocation 例項,你可以使用方法 copyFrom()copyTo() 從這些 Allocations 複製資料。例如,你可以通過呼叫將新影象複製到輸入分配中:

inputAllocation.copyFrom(inputBitmap);

通過在輸出 Allocation 上呼叫 copyTo() 來檢索結果影象的方法相同:

outputAllocation.copyTo(outputBitmap);

建立分配例項

有很多方法可以建立一個 Allocation。一旦你有了一個 Allocation 例項,你可以使用 copyTo()copyFrom() 從這些 Allocations 複製新資料,如上所述,但是要建立它們,你必須知道你正在使用什麼型別的資料。讓我們從輸入 Allocation 開始:

我們可以使用靜態方法 createFromBitmap()Bitmap 快速建立我們的輸入 Allocation

final Allocation inputAllocation = Allocation.createFromBitmap(renderScript, image);

在此示例中,輸入影象永遠不會更改,因此我們永遠不需要再次修改輸入 Allocation。每次 saturationLevel 更改以建立新輸出 Bitmap 時,我們都可以重複使用它。

建立輸出 Allocation 要複雜一點。首先,我們需要建立一個叫做 Type 的東西。Type 用於告訴 Allocation 它正在處理什麼樣的資料。通常一個人使用 Type.Builder 類來快速建立一個合適的 Type。我們先來看看程式碼:

final Type outputType = new Type.Builder(renderScript, Element.RGBA_8888(renderScript))
        .setX(inputBitmap.getWidth())
        .setY(inputBitmap.getHeight())
        .create();

我們使用普通的 32 位(或換句話說 4 位元組)每畫素 Bitmap 使用 4 個顏色通道。這就是我們選擇 Element.RGBA_8888 來創造 Type 的原因。然後我們使用方法 setX()setY() 將輸出影象的寬度和高度設定為與輸入影象相同的大小。然後 create() 方法用我們指定的引數建立 Type

一旦我們有了正確的 Type,我們就可以用靜態方法 createTyped() 建立輸出 Allocation

final Allocation outputAllocation = Allocation.createTyped(renderScript, outputType);

現在我們差不多完成了。我們還需要一個輸出 Bitmap,我們可以從輸出 Allocation 複製資料。為此,我們使用靜態方法 createBitmap() 建立一個新的空 Bitmap,其大小和配置與輸入 Bitmap 相同。

final Bitmap outputBitmap = Bitmap.createBitmap(
        inputBitmap.getWidth(),
        inputBitmap.getHeight(),
        inputBitmap.getConfig()
);

有了這個,我們就擁有了執行 RenderScript 的所有拼圖。

完整的例子

現在讓我們把所有這些放在一個例子中:

// Create the RenderScript instance
final RenderScript renderScript = RenderScript.create(context);

// Create the input Allocation 
final Allocation inputAllocation = Allocation.createFromBitmap(renderScript, inputBitmap);

// Create the output Type.
final Type outputType = new Type.Builder(renderScript, Element.RGBA_8888(renderScript))
        .setX(inputBitmap.getWidth())
        .setY(inputBitmap.getHeight())
        .create();

// And use the Type to create am output Allocation
final Allocation outputAllocation = Allocation.createTyped(renderScript, outputType);

// Create an empty output Bitmap from the input Bitmap
final Bitmap outputBitmap = Bitmap.createBitmap(
        inputBitmap.getWidth(),
        inputBitmap.getHeight(),
        inputBitmap.getConfig()
);

// Create an instance of our script
final ScriptC_saturation script = new ScriptC_saturation(renderScript);

// Set the saturation level
script.set_saturationLevel(2.0f);

// Execute the Kernel
script.forEach_saturation(inputAllocation, outputAllocation);

// Copy the result data to the output Bitmap
outputAllocation.copyTo(outputBitmap);

// Display the result Bitmap somewhere
someImageView.setImageBitmap(outputBitmap);

結論

通過這個介紹,你應該已經準備好編寫自己的 RenderScript 核心以進行簡單的影象處理。但是,你必須記住以下幾點:

  • RenderScript 僅適用於應用程式專案 :當前 RenderScript 檔案不能是庫專案的一部分。
  • 注意記憶體 :RenderScript 非常快,但也可能是記憶體密集型。任何時候都不應該有超過一個 RenderScript 的例項。你還應該儘可能多地重用。通常,你只需要建立一次 Allocation 例項,並在將來重用它們。輸出 Bitmaps 或你的指令碼例項也是如此。儘可能多地重複使用。
  • 在後臺完成你的工作 :RenderScript 再次非常快,但不是任何方式。任何核心,尤其是複雜的核心都應該在 AsyncTask 中的 UI 執行緒中執行。但是在大多數情況下,你不必擔心記憶體洩漏。所有與 RenderScript 相關的類僅使用應用程式 Context,因此不會導致記憶體洩漏。但你還是要擔心通常的事情,如洩漏 ViewActivity 或你自己使用的任何 Context 例項!
  • 使用內建的東西 :有許多預定義的指令碼執行影象模糊,混合,轉換,調整大小等任務。還有許多內建方法可以幫助你實現核心。如果你想要做某事,可能有一個指令碼或方法已經完成了你正在嘗試做的事情。不要重新發明輪子。

如果你想快速入門並使用實際程式碼,我建議你檢視示例 GitHub 專案,該專案實現了本教程中討論的確切示例。你可以在這裡找到這個專案。享受 RenderScript 的樂趣吧!