第1章 RenderScript
RenderScript提供一个独立于平台并在本地运行的计算引擎,用它来加速你需要大量计算能力的应用。RenderScript是一个运行与Android上计算密集型的高性能架构。RenderScript主要是面向数据的并行计算,虽然串行计算密集型负载也不错。RenderScript运行时将在所有可用的处理器上并行工作,如多核CPU,GPU或DSP,让你专注于算法而不是调度工作或负载均衡。RenderScript对于图像处理,计算摄影或计算机视觉这样的应用程序尤其有用。对于RenderScript,你先要明白两个概念:
1.高性能计算内核由C99语言编写
2.Java API用于管理RenderScript的资源寿命和控制核心的执行
1.1 写一个RenderScript内核
一个RenderScript内核通常驻留在一个.rs文件中,它在
/src/目录下,每一个rs文件被叫做一个脚本。每个脚本包含它自己的一套内核,函数和变量。一个脚本可以包含:
◆编译指示声明(#pragma version(1))用来声明一个RenderScript内核语言的版本。目前,1是唯一的有效值
◆编译指示声明(#pragma rs java_package_name(com.example.app))用来声明一个Java类的包名
◆一些调用函数。一个调用函数是一个单线程RenderScript函数,你能从你的java代码中使用任意参数来调用。这些经常用于初始化设置或在一个巨大的处理管道内进行串行计算。
◆一些脚本的全局变量,一个脚本的全局变量等价于一个在C中的全局变量。你能从java代码中访问脚本全局变量,并且这些变量经常被用于参数传递到RenderScript内核中。
◆一些计算内核,一个内核是一个并行函数,执行配置中的每一个元素。
下面是一个简单的例子:如代码清单1-1所示:
uchar4 __attribute__((kernel)) invert(uchar4 in, uint32_t x, uint32_t y) {
uchar4 out = in;
out.r = 255 - in.r;
out.g = 255 - in.g;
out.b = 255 - in.b;
return out;
}
代码清单1-1
在许多方面,这是一个标准的C函数。第一个显著的特点是__attribute__((kernel))应用函数原型。这是指此函数是一个RenderScript内核而不是调用函数。接下来的特性是在参数及其类型。在RenderScript内核,这是一个特殊的参数,它自动填充基于输入配置传递给内核启动。默认情况下,内核跨整个配置运行,在配置中每个元素都带一个内核执行体。第三个显著的特性是内核的返回类型。在输出配置中,返回的值从内核自动写入到适当的位置。RenderScript运行时检查以确保输入和输出元素配置匹配内核原型,如果它们不匹配,则抛出一个异常。
一个内核可以有一个输入配置或一个输出配置,或两者都有。一个内核可能没有一个以上的输入或输出配置。如果一个以上的输入和输入是必须的,那些对象将结合rs_allocation
脚本全局变量并通过rsGetElementAt_type()或rsSetElementAt_type()从一个内核或者调用函数访问。一个内核可以使用X,Y,Z参数访问当前执行的坐标。这些参数是可选的,但坐标的参数类型必须是uint32_t
一个可选的init()函数。一个init()函数是一种特殊类型的调用函数,当脚本首次实例化时,需要调用它。这使得一些计算自动发生在脚本创建时。
一些静态脚本全局变量和函数。静态脚本全局变量几乎等价于脚本全局变量,只是静态不允许从java代码调用。静态函数是一个标准的C函数,它能被任意内核或调用函数调用,而不用暴露在Java API。如果一个脚本全局变量或函数不需要从java代码中调用,那么强烈建议声明为静态的。
设置浮点精度
你可以在一个脚本中控制所需的浮点精度,如果完整的IEEE 754-2008标准是有用的。那么以下编译指示可以设置一个不同的浮点精度等级:
◆#pragma rs_fp_full (默认如果没有什么被指定): 应用程序需要通过IEEE754-2008标准的浮点精度概述。
◆#pragma rs_fp_relaxed应用程序不需要严格的IEEE 754-2008标准并可以允许不精确,对于de-norms计算这种模式启动了清零(flush-to-zero),并且向零舍入(round-towards-zero)。
◆#pragma rs_fp_imprecise 应用程序不具有严格的精度要求。这种模式可以使一切rs_fp_relaxed 按照以下操作:
操作导致-0.0被+0.0代替返回
操作在INF和NAN在未定义。
大多数应用程序可以使用relaxed无任何副作用。这对于一些只提供relaxed精度的架构并需要额外优化的情况来说,这是非常有益的。(如SIMD CPU指令)
1.2 访问RenderScript API
当开发使用RenderScript开发Android应用程序时,你能通过以下其中一种方式来访问API:
◆android.renderscript这个API在Android3.0以上是可用的,这些对于RenderScript都是原始API并且当前没有更新。
◆android.support.v8.renderscript这个API需要通过Support Library才可用,这样允许你在Android2.2或更高版本中使用他们
官方强烈推荐使用Support Library API来访问RenderScript。因为它们包含计算框架最新的改进以及支持更广泛的设备兼容性。下面我们来使用RenderScript Support Library APIs
为了使用这个API,必须配置我们的开发环境。首选需要Android SDK Tool为22.2或更高。Android SDK Build 为18.1.0或更高。你能通过Android SDK Manager来更新安装版本。注意RSSL(RenderScript Support Library)API目前不支持Android Studio或Gradle编译。下面我们将在Eclipse中使用RSSL:
1.确保你有必要的Android SDK版本和编译工具版本
2.打开项目中的project.properties文件
3.添加以下几行到文件中
renderscript.target=18
renderscript.support.mode=true
sdk.buildtools=18.1.0
4.在你的类中使用RenderScript,导入Support Library:
import android.support.v8.renderscript.*;
project.properties设置了以上几行来控制Android在编译时的行为:
◆renderscript.target指定生成的字节码版本。官方推荐你设置这个值为可用的最高值并且设置renderscript.support.mode为true。从11到最新的API LEVEL在这之间设置任意整形值都是合法的。如果你的最小SDK版本在manifest中指定的值也为当前的SDK版本的较高值,那么renderscript.target将被忽略并且将目标值设置为最小SDK版本。
◆renderscript.support.mode如果它在不支持目标版本的设备上运行时,指定生成的字节码回滚兼容版本
◆sdk.buildtools指Android SDK编译工具使用的版本。这个值被设置为18.1.0或更高,如果你不指定这个值,会使用你当前最高安装的编译工具版本。你应该设置这个值,确保在不同的开发机器上保持一致性
1.3 在Java代码中使用RenderScript
在Java代码中使用RS需要依赖android.renderscript或android.support.v8.renderscript包。大多数应用程序遵循基本的使用模式
1.初始化一个RenderScript的context。这个RenderScript context在create(Context)中被创建,确保RenderScript能被使用并且提供一个对象来控制后来的RenderScript对象的生命周期。你应该考虑context创建与一个潜在的长时间运行的操作,因为它可能在不同的硬件块创建资源,如果可能的话,它不应该在应用程序的关键路径。通常,一个应用程序在一个时间内只有一个单一的RenderScript context。
2.通过一个叫脚本创建至少一个Allocation。一个Allocation是一个RenderScript对象,它提供一个固定的数据量存储。在脚本中的内核带来Allocation对象作为它们的输入输出,当作为脚本全局变量绑定时,并且Allocation对象在内核中能被rsGetElementAt_type()和 rsSetElementAt_type()访问。Allocation对象允许从java代码到RenderScript代码传递数组,相反的方向也可以。Allocation对象通常使用createTyped(RenderScript, Type)或createFromBitmap(RenderScript, Bitmap)来创建。
3.创建任意脚本是必要的。在使用RenderScript时,有2种脚本类型可用:
ScriptC:这些都是用户定义的脚本描述在上面(1.1写一个RenderScript内核)小节中。每个脚本有一个Java类反射,通过RenderScript编译器可以很容易的从java代码中访问脚本;这个类命名被ScriptC_filename。例如如果内核位于invert.rs中并且RenderScript context已经位于mRS中,java代码实例化脚本的方法如下:
ScriptC_invert invert = new ScriptC_invert(mRenderScript);
ScriptIntrinsic:这些是常见的操作:如高斯模糊,回旋,图像融合。更多信息,请参考ScriptIntrinsic的子类。
4.数据填充Allocation。除了用android.renderscript创建Allocation,在首次创建时一个Allocation也能被空数据填充。为了填充一个Allocation,在Allocation里使用一个copy方法。
5. 设置必要的脚本全局变量。全局变量命令规则最好使用set_globalname。例如,为了设置一个int类型的elements名字变来那个,使用java方法set_elements(int)。RenderScript对象在内核中也能被设置,例如,rs_allocation变量命名为lookup,能被方法set_lookup(allocation)设置。
6.启动适当的内核。方法来启动一个给定的内核将被反射到同样的ScriptC_filename类使用名为forEach_kernelanme()的方法。这些启动器是异步的,并且启动将按照他们的启动顺序被序列化。依赖你内核参数,这个方法将带来一个或者两个Allcation。默认情况下,一个内核将执行整个输入或输出Allcation;通过适当的Script.LaunchOptions作为最后一个参数到forEach方法来执行完一个Allcation的子集。调用函数能使用invoke_functionname方法来启动,在同样的ScriptrC_filename类中被反射。
7.出于Allocation对象复制数据。为了使用java代码从Allocation中访问数据,数据必须在Allocation中使用一个copy方法复制到java缓冲区, 这些函数将和异步内核一起同步并作为必要的函数启。
8.拆除RenderScript context。RenderScript context能通过destroy()方法或通过垃圾回收来摧毁。这将导致后面使用任意附属到context的对象抛出一个异常。