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 的乐趣吧!