DSP

RenderScript的基础使用

2019-07-13 19:22发布

Renderscript是Android操作系统上的一套API。它基于异构计算思想(指使用不同类型指令集和体系架构的计算单元组成系统的计算方式),专门用于密集型计算,尤其是图像处理、计算机图形、计算机视觉。允许开发者以较少的代码实现功能复杂且性能优越的应用程序。 RenderScript可在设备上所有可用的处理器上并行执行,例如多核CPU、GPU、DSP,所以开发者可以专心写处理算法,而不需要关心调度和负载平衡的问题。 本文通过写一个简单的例子(源码链接)来解释怎么使用RenderScript,例子中将彩 {MOD}的图片转换为黑白的,为了测试RenderScript的性能,加上手指滑动的效果,如下图: 2031862-932fba7e7be46635.gif

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 这个链接是旧的版本,但是里面有些内容是前面那个没提到的。