Android高效计算——RenderScript(一)
高效计算——RenderScript
RenderScript是安卓平台上很受谷歌推荐的一个高效计算平台,它能够自动把计算任务分配到各个可用的计算核心上,包括CPU,GPU以及DSP等,提供十分高效的并行计算能力。可能是由于应用开发时的需求不够,关于RenderScript的相关文章很少,刚好我在工作中应用到此平台,做了一些笔记,因此决定整理成博文分享给大家。内容主要来源于官方文档、StackOverflow以及自己的理解,如有错误,请大家指正。本篇主要介绍RenderScript的基本概念。
1 RenderScript简介
RenderScript是安卓提供的一个高效计算平台。它显著的特点在于:
- 能够自动利用各种核心,包括CPU,GPU以及DSP等,来进行并行计算,能大大提高在图片处理、数学模型等领域提供高效的计算能力;
- 不需要针对不同的核心平台而编写不同的代码,因为RenderScript是在设备上进行运行时编译的。
使用了RenderScript的应用与一般的安卓应用在代码编写上与并没有太大区别。使用了RenderScript的应用依然像传统应用一样运行在VM中,但是你需要给你的应用编写你所需要的RenderScript代码,且这部分代码运行在native层。
RenderScript采用从属控制架构:底层RenderScript被运行在虚拟机中的上层安卓系统所控制。安卓VM负责所有内存管理并把它分配给RenderScript的内存绑定到RenderScript运行时,所以RenderScript代码能够访问这些内存。安卓框架对RenderScript进行异步调用,每个调用都放在消息队列中,并且会被尽快处理。
RenderScript工作流程需要经历三层:
- RenderScript运行时API:提供进行运算的API
- 反射层:相当于NDK中的JNI胶水代码,它是一些由安卓编译工具自动生成的类,对你写的RenderScript代码进行包装,使得安卓层能够和RenderScript进行交互
- 安卓框架:通过调用反射层来访问RenderScript运行时
RenderScript的主要优点:
- 可移植性:对于不同架构,不同的处理器都不需要考虑代码的差异化,因为都是运行时在设备上进行编译的;
- 高性能:提供充分利用所有核心的无缝的并行化计算
- 易用性:简化编码,不需要像JNI一样写胶水代码
缺点:
- 开发复杂:需要去学习新的api
- 调试可见性:因为RenderScript可能运行在除了主cpu之外的处理器上,所以调试困难
2 使用RenderScript
使用RenderScript需要对编译或者开发环境进行一定的配置。
使用RenderScript主要分为两个步骤:编写.rs文件以及在Android framework中使用RenderScript,下面分别介绍。
2.1 环境配置
- RenderScript的API可以有两种来源方式:
对于Android 3.0 (API level 11)及以上的可以在android.renderscript包中获取
通过android.support.v8.renderscript包获取,可以支持API level 8及以上的平台,官方强烈建议使用此支持包的方式来获取API
- 编译环境要求:
Android SDK Tools revision 需要22.2及以上
Android SDK Build-tools revision 需要18.1.0及以上
- 在project.properties文件中写入如下属性:
renderscript.target=18 renderscript.support.mode=true
或者在AS中的build.gradle的defaultConfig中添加
renderscriptTargetApi 18 renderscriptSupportModeEnabled true
注意:target的值应该为11及以上,但推荐使用18.如果在Manifest中配置的minSDK的值与target的值不相同,那么在编译的时候,将使用target的值替代Mainfest中的minSDK值。
2.2 编写RenderScript文件
RenderScript代码放在.rs或者.rsh文件中,在RenderScript代码中包含计算逻辑以及声明所有必须的变量和指针,通常一个.rs文件包含如下几个部分:
- 编译声明:#pragma rs java_package_name(package.name),比如#pragma rs java_package_name (com.willhua.RenderScript),用来声明本rs所在的java包。注意:.rs文件只能在应用程序包中,而不能在library项目中。
- 编译声明:#pragma version(1).声明RenderScript版本,现在都是1
- 主工作函数root().它会被RenderScript层的reForEach函数调用,实现多处理器对root工作的并行处理。Root函数必须返回void以及接受如下参数
1.分配给RenderScript的输入输出地址的指针。在Android3.2以及更低版本中,输入输出的指针都需要,在Android4.0及以后的版本中,给出其中一个或者两个都可以
- 2.下面两个参数是可选的,但是只要用了其中一个就必须两个都提供
a) 指向用户数据的指针。该数据会在RenderScript的计算中用到。该数据可以指向原始类型或者复杂结构类型
b) 用户数据的大小
从官方文档来看,老版本的文档中有介绍root,而新版本的则用kernel替代。官方在弱化root函数的概念,而是推荐使用kernel概念。本质上来说,root仅仅是一个写法形式上特殊的kernel而已。
- 可选init()函数。可以用来做任何初始化工作,比如初始化变量。它将会在每次RenderScript启动的时候,在其他任何代码之前执行一次
- 一些invokable函数。这些函数都是单线程函数(kernel函数的工作则是并行工作的),你可以给这些函数传递任意数量的参数。这些函数将会在反射层中生成对应的版本,可以从Android framework中调用。这些函数一般用来做一些初始化工作或者当做计算任务中的一个串行计算单元任务。注意:invokable函数不能是static的。
- 一些计算内核(compute kernel)。计算内核是并行执行的,它将并行处理输入Allocation中的每一个Element。一个简单的compute kernel如下:
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; }
compute kernel基本与一个C函数一样,但是有如下特征:
a) __attribute__((kernel))标志。该标志表示该函数是一个RenderScript kernel函数,而不是一个invokable函数
b) in参数及其类型。在RenderScript kernel中,这个参数将会基于传给kernel的输入Allocation而自动赋值,且默认情况下,对于Allocation中每一个Element都将会执行一遍kernel函数
c) 返回值及其类型。每次kernel函数执行的返回值将会自动写入到输出Allocation的正确位置。RenderScript将会对输入输出Allocation进行检查,如果他们与kernel函数声明不匹配则将抛出异常。
每个kernel都应该有一个输入Allocation或者一个输出Allocation或者二者都有,但不能有两个及以上的输入或者输出Allocation。如果需要在kernel中访问多个输入或者输出,则需要声明rs_allocation全局变量来担任多余一个的输入或者输出角色,然后再kernel函数或者invokable函数中通过rsGetElementAt_type()或者rsSetElementAt_type()来访问或者设置相应的Allocation,其中type为对应Allocation的Element类型对应的数据类型,比如uchar4。
在kernel中,可以通过可选的xyz参数来获取当前Element在整个Allocation中的坐标值,比如上面的invert中就通过xy来获取了xy坐标值。注意xyz的参数名不能设置为其他名称,且类型必须为uint32_t。
- 任何要在RenderScript中用到的变量,指针以及结构。这些声明也可以在.rsh文件中
- 所需要的script变量。就和C中的全局变量一样,这些全局变量一般用来传递参数给计算kernel。
- 一些静态变量以及函数。静态的变量与普通全局变量的区别在于:静态变量不会在映射在反射层,也即无法从Android framework中调用;静态函数就是一个标准的C函数,但是不会映射到反射层,也无法从Android framework中调用,但是可以在RenderScript中的kernel或者invokable中调用。如果有变量或者函数需要在RenderScript中使用但是不需要在Java中使用,强烈推荐设置为static的。
- 可选的精度控制配置,主要有三个等级:
a) #pragma rs_fp_full:默认的等级。表示的完全遵守IEEE 754-2008 standard的精度要求
b) #pragma rs_fp_relaxed:不严格的IEEE 754-2008 standard的精度要求
c) #pragma rs_fp_imprecise:比relaxed更低的精度要求
对于大部分应用来说,使用relaxed精度要求都可以满足要求而无任何副作用
example.rs :
#pragma version(1) #pragma rs java_package_name(com.willhua.rgbtoyuv) #pragma rs_fp_relaxed typedef struct Point_T{ int x; int y; }Point; //script variable uint32_t inW; uint32_t inH; uint32_t inCount; rs_allocation outYUV; struct Point point; //root void root(const uchar4 *in, uint32_t x, uint32_t y){ struct myStruct my; my.x = 0; struct myStruct my2 = my; int u = my.x; uchar R,G,B; int Y,U,V; R = (*in).r; G = (*in).g; B = (*in).b; Y = ( ( 66 * R + 129 * G + 25 * B + 128) >> 8) + 16; uint32_t yIndex = y * inW + x; Y = ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y)); rsSetElementAt_uchar(outYUV, ((uchar)Y), yIndex); if((x & 1) == 0 && (y & 1) == 0) { U = ( ( -38 * R - 74 * G + 112 * B + 128) >> 8) + 128; V = ( ( 112 * R - 94 * G - 18 * B + 128) >> 8) + 128; uint32_t index = (y >> 1) * inW + x + inCount; U = ((U < 0) ? 0 : ((U > 255) ? 255 : U)); V = ((V < 0) ? 0 : ((V > 255) ? 255 : V)); rsSetElementAt_uchar(outYUV, ((uchar)U), index + 1); rsSetElementAt_uchar(outYUV, ((uchar)V), index ); } } //compute kernel __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; } //invokable function void setInPara(uint32_t w, uint32_t h){ inW = w; inH = h; inCount = w * h; } //init void init(){ }
2.3 在Android framework层调用RenderScript
虽然各个应用使用RenderScript细节各不相同,但大体有着这样的模式:
- 初始化RenderScript context。通过RenderScript.create函数可以创建相应的context,有了该context才可以进行RenderScript的其他动作,并通过该context可以控制其他RenderScript对象的生命周期。因为context的创建需要在不同的硬件设备上创建资源,所以可能会比较耗时。因此,最好不要让context的创建在响应速度要求比较高的时间点上。一般来说,一个应用应该只有一个context。
- 至少需要创建一个Allocation。用的比较多的方法是createTyped(RenderScript, Type) 或createFromBitmap(RenderScript, Bitmap)。
- 创建需要的scripts。可以分为两种,一种就是通过自定义.rs文件,然后在反射层自动生成的ScriptC子类,名字为ScriptC_rsfilename;还有一种就是系统定义的一些scripts,比如高斯模糊等,他们都是ScriptIntrinsic的子类。
- 给Allocation填充数据。使用Allocation的copy系类方法
- 设置必要的script变量。在ScriptC_rsfilename会给script变量生成相应的set方法,比如int型名为num的script变量则会在反射层生成set_num(int)方法。
- 启动计算。.rs中定义的kernel函数都会在反射层的ScriptC_rsfilename类中生成forEach_kernalname方法。该方法的执行是异步的,且会在RenderScript中按照kernel调用顺序来执行。前面提到过,forEach_kernalname方法默认对对应Allocation中所有的Element执行计算,但是可以在forEach_kernalname参数的最后传入一个Scrpit.LunchOptions参数来指定Allocation中需要被计算的子集。.rs中定义的invokable函数则会在反射层生成invoke_functionname的方法,也可以按需调用
- 从Allocation中得到数据。通过Allocation的copyTo系类方法,可以把Allocation中的数据放到Java数据中。这些copyTo方法与forEach方法保持同步,forEach的计算完成之后就会自动启动copy过程。目前只支持基本类型的copyTo,暂不支持比如自定义的struct等数据。
- 释放资源。通过RenderScript.destory方法来释放相关资源。
2.4 RenderScript工作流程
最开始就提到,RenderScript是一个主从架构,底层的RenderScript被上层的Android framework所控制。其工作流程也正是如此。从在Android framework创建RenderScript的context开始,然后给RenderScript层分配、绑定相关内存,对script变量进行初始化,然后调用forEach函数通知启动RenderScript计算。RenderScript将会自动把它的计算任务分配到各个可用的核心上来完成计算任务(现在还只能支持CPU,以后将会支持到GPU以及DSP,且代码不需要变动)。RenderScript计算完成以后将会自动把计算结果放到相应的Allocation内存,然后在Android framework层再从Allocation中copy出数据,最后Android framework层命令RenderScript释放资源,流程介绍。下图展示了RenderScript的工作流程: