Renderscript是Android操作系统上的一套API。它基于异构计算思想(指使用不同类型指令集和体系架构的计算单元组成系统的计算方式),专门用于密集型计算,尤其是图像处理、计算机图形、计算机视觉。允许开发者以较少的代码实现功能复杂且性能优越的应用程序。
RenderScript可在设备上所有可用的处理器上并行执行,例如多核CPU、GPU、DSP,所以开发者可以专心写处理算法,而不需要关心调度和负载平衡的问题。
本文通过写一个简单的例子(
源码链接)来解释怎么使用RenderScript,例子中将彩 {MOD}的图片转换为黑白的,为了测试RenderScript的性能,加上手指滑动的效果,如下图:
1. 工程配置
RenderScript的api从Android 3.0(Api level 11)开始系统自带了,在低于3.0的机器上,可以通过support包来使用RenderScript。但是还是推荐都使用support的RenderScript,因为RenderScript可能会有bug,使用support包能够及时得到更新。本文均使用support包中的Api。
要使用RenderScript,除了要导入一个jar包外,还需要复制一些so文件,如果使用gradle的话,就方便一些,只需要在build.gradle文件中增加两句:
android {
compileSdkVersion 19
buildToolsVersion "19.0.3"
defaultConfig {
minSdkVersion 8
targetSdkVersion 16
renderscriptTargetApi 18
renderscriptSupportModeEnabled true
}
}
上面两句设置会导致在android编译的时候,有一些特殊的流程:
-
renderscriptTargetApi:设置生成的字节码的版本,推荐使用最高的API Level,并且设置renderscriptSupportModeEnabled为true。这个选项的合法值从11到最新发布的API Level。如果你的最低SDK的版本和这个值不一样,这个值就会被忽略,在编译的时候,这个值会被设置了最低的SDK版本。
-
renderscriptSupportModeEnabled:指定在运行的设备不是target version的时候,生成的字节码需要回滚到兼容的版本。
-
buildToolsVersion:需要使用的Android SDK的编译工具的版本,这个值需要设置为18.1.0或者更高。如果这个值没有指定,那么将使用当前已安装的最高版本。这个值最好设置,防止不同开发者的机器配置不同,导致奇怪的编译问题。
2. 脚本语言
为了能在多个平台执行,RenderScript要求开发者使用Rs的脚本语言来实现计算的代码,脚本代码采用了c99语法,所以看起来和C语言很像。代码要求放在.rs文件中,文件需要放在
/src/ 中,脚本包含脚本的入口、函数和变量:
#pragma version(1)
#pragma rs java_package_name(com.winomtech.androidmisc.rs)
void root(const uchar4 *in, uchar4 *out, uint32_t x, uint32_t y) {
out->r = out->g = out->b = (in->r + in->g + in->b) / 3;
}
void init() {
}
#pragma version(1) 声明脚本中使用的Rs的版本。
#pragma rs java_package_name 声明脚本生成的Java层的代码所在的包名,这个将在后面再细讲。
root(Kernel)函数是rs脚本默认的入口,这种入口函数在rs中称为Kernel函数,默认的Kernel函数名为
root,且必须返回
void,并且有以下的参数:
- 指向rs脚本输入输出的内存的指针。在Android 3.2(Api Level 13)或者更早的版本,这两个参数都需要,Android 4.0(Api Level 14)及以后的版本要求一个或两个。
下面的参数是可选的,但是如果你选择使用它们,则都需要提供:
- rs脚本用来实现计算的附加数据的指针,可以是原始类型的指针,也可以是复杂的结构体的指针。
- 附加数据的大小
从Android 4.1 (Api level 16)开始,可以自己定义kernel函数的参数,而不必和前面描述参数及返回值一样。并且可以在同一个脚本中定义多个kernel函数,但是需要在自定义的kernel函数前面增加 __attribute__((kernel))。例如,下面是一个kernel函数,接收两个 uint32_t 类型的参数,返回一个uchar4:
uchar4 __attribute__((kernel)) root(uint32_t x, uint32_t y) {
...
}
参数类型uchar4是rs中一类数据类型,称为Vector。Vector通常是普通类型接着2、3、4,比如:float4, int3, double2, ulong4。Vector类型中的成员可以使用多种风格来访问:变量名接着一个点,再接着:
- 字符x、y、z和w
- 字符r、g、b和a
- 字符s或S,随后接着从0开始的下标
uint32_t x、uint32_t y两个参数是当前执行到的下标,这两个参数是可选的,最多可以有三维x、y、z,类型必须为uint32_t。
init函数是一个可选的函数,在kernel函数执行前,init函数会被执行一次,可以在init里面做一些初始化的工作,比如初始化变量。
3. 脚本编译
脚本在执行之前会有两次编译:在android编译的过程中,会发生第一次编译,生成LLVM的字节码;当应用在设备中执行的时候,会发生第二次编译,生成设备的机器码。
4. Java层的接口
因为脚本最后会被编译为机器码,为了方便Java层设置数据和调用接口,rs在android编译的时候,会生成一个java文件,名字为ScriptC_
script_name,比如Gray.rs文件,会自动生成一个ScriptC_Gary.java。下面代码是上面rs脚本生成的内容:
public class ScriptC_Gray extends ScriptC {
private static final String __rs_resource_name = "gray";
// Constructor
public ScriptC_Gray(RenderScript rs) {
this(rs,
rs.getApplicationContext().getResources(),
rs.getApplicationContext().getResources().getIdentifier(
__rs_resource_name, "raw",
rs.getApplicationContext().getPackageName()));
}
public ScriptC_Gray(RenderScript rs, Resources resources, int id) {
super(rs, resources, id);
__U8_4 = Element.U8_4(rs);
}
private Element __U8_4;
private final static int mExportForEachIdx_root = 0;
public Script.KernelID getKernelID_root() {
return createKernelID(mExportForEachIdx_root, 27, null, null);
}
public void forEach_root(Allocation ain, Allocation aout) {
forEach_root(ain, aout, null);
}
public void forEach_root(Allocation ain, Allocation aout, Script.LaunchOptions sc) {
// check ain
if (!ain.getType().getElement().isCompatible(__U8_4)) {
throw new RSRuntimeException("Type mismatch with U8_4!");
}
// check aout
if (!aout.getType().getElement().isCompatible(__U8_4)) {
throw new RSRuntimeException("Type mismatch with U8_4!");
}
Type t0, t1; // Verify dimensions
t0 = ain.getType();
t1 = aout.getType();
if ((t0.getCount() != t1.getCount()) ||
(t0.getX() != t1.getX()) ||
(t0.getY() != t1.getY()) ||
(t0.getZ() != t1.getZ()) ||
(t0.hasFaces() != t1.hasFaces()) ||
(t0.hasMipmaps() != t1.hasMipmaps())) {
throw new RSRuntimeException("Dimension mismatch between parameters ain and aout!");
}
forEach(mExportForEachIdx_root, ain, aout, null, sc);
}
}
生成的代码继承于ScriptC,其中有一些变量和函数名都有_root的后缀,这个后缀的root其实就是kernel函数的名字,如果一个rs脚本中有多个kernel函数,比如有一个名为test的kernel函数,那么就有一些函数和变量名有_test后缀。
getKernelID_root这个是返回root的id,这个id是RenderScript框架所使用的。有时候需要在一个Kernel函数执行完后,再执行另外一个时,就需要使用到它。
forEach_root函数是用来调用Kernel函数执行的入口,在执行之前,会检查输入和输出是否和脚本中的类型一致,如果不一致直接抛出异常,同时还会检查输入和输出的维度是否一致。
5. 执行
下面代码是缩减后的代码,仅作讲解使用,代码的主要作用是用输入的图片生成一张黑白的图片,并且显示在界面上:
Bitmap mInBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.renderscript_input);
Bitmap mOutBitmap = Bitmap.createBitmap(mInBitmap.getWidth(), mInBitmap.getHeight(), Bitmap.Config.ARGB_8888);
RenderScript mRenderScript = RenderScript.create(getActivity());
ScriptC_Gray mGrayScript = new ScriptC_Gray(mRenderScript);
Allocation mAllocationIn = Allocation.createFromBitmap(mRenderScript, mInBitmap);
Allocation mAllocationOut = Allocation.createFromBitmap(mRenderScript, mOutBitmap);
mGrayScript.forEach_root(mAllocationIn, mAllocationOut);
mAllocationOut.copyTo(mOutBitmap);
mImageView.setImageBitmap(mOutBitmap);
RenderScript
这个类用来访问RenderScript的上下文,管理RenderScript的初始化、资源、以及生命周期的。在任何其他RS对象使用之前,需要创建一个RenderScript对象,并且需要缓存这个对象,以方便后续使用。当进程完成使用RenderScript的任务的时候,需要调用releaseAllContexts。
通过调用RenderScript.create静态方式,可以得到RenderScript的对象。在Api 23的时候,多次调用create得到的会是同样的对象,所以能够得到同样的上下文,以及同样的配置。在Api 23之前,每次调用create将生成一个新的上下文。
ScriptC_Gray
上文已讲到。
Allocation
应用程序在Android虚拟机上使用RenderScript,但是实际的rs脚本在native层执行,并且需要访问Android虚拟机分配的内存。为此,就需要将虚拟机层分配的内存和RenderScript runtime连接起来,这个过程称为绑定,然后RenderScript runtime就可以方便的使用它需要的内存了,而不需要显式的分配。最终的结果就好像你在C中调用了malloc一样。
为了支持这种内存分配的方式,提供了一组API给Android VM来分配内存,类似于malloc函数的功能。这些类本质是描述应该怎么分配内存以及执行真正的分配。为了更好的理解这些类是怎么工作的,我们看看它们与malloc的关系:
array = (int *)malloc(sizeof(int)*10);
这个malloc调用可以分为两个部分:分配的内存单元的大小(sizeof(int)),分配的单元的个数。Android框架提供了类似于这两部分的类,以及类似malloc的类。
Element类代表malloc中的分配单元,例如浮点类型或者一个结构体。Type类对Element和分配的个数进行包装,可以将Type想象成Element的数组。而Allocation类基于给定的Type执行真正的内存分配,以及访问分配的内存。
Type由5个维度组成:X、Y、Z、LOD(level of detail)和Faces(of a cube map)。可以设置X、Y、Z为任意正整数,但是要限制在可用内存的范围内。如果某个维度的值为0,表示没有那个维度,例如:x=10、y=1、z=0表示二维。LOD和Faces用boolean值来表示是否出现了。
Allocation.createFromBitmap是对分配操作的封装,会根据Bitmap的配置来指定对于的Element的值,以及Type的值,并且用Bitmap的宽高来指定x、y的值。
forEach_root
forEach_root中两个参数分别指定输入和输出,RenderScript会根据输入参数中的维度,遍历整个数组,对每个数值都调用一次Kernel函数,所以rs文件中的in、out、x、y是根据当前遍历的位置的不同而不同。
6. 变量
在前面的rs文件中,只指定了输入和输出,当需要外部传入多个参数时,就需要使用rs的变量了。本文的例子中,除了要指定输入输出,还需要指定从哪个位置开始是需要变灰的,所以我们在rs脚本中增加一个变量:
#pragma version(1)
#pragma rs java_package_name(com.winomtech.androidmisc.rs)
int gPos;
...
再编译下android工程,我们再看生成的java代码,会多出来两个方法:
public synchronized void set_gPos(int v) {
setVar(mExportVarIdx_gPos, v);
mExportVar_gPos = v;
}
public int get_gPos() {
return mExportVar_gPos;
}
java层就可以通过这两个接口来访问rs中的gPos变量了。
7. 总结
RenderScript用起来还算挺简单的,当然也主要是本文实现功能简单。当然RenderScript的作用还是很容易看出来的,可以写一个长循环,然后看cpu使用,在我的手机上面,4个核心可以跑满,而如果使用jni的话,很明显只会在一个核心上面跑。
参考资料:
http://developer.android.com/guide/topics/renderscript/index.html
https://stuff.mit.edu/afs/sipb/project/android/docs/guide/topics/renderscript/index.html 这个链接是旧的版本,但是里面有些内容是前面那个没提到的。