RenderScript
RenderScript是Android系统中能高效处理大量计算任务的框架,特别适用一些需要处理图片和加载图片以及计算机视觉的方面应用。
本文将先从如何使用出发,然后介绍关于 RenderScript
的部分高级知识。
在阅读本文之前,需要知道的是Renderscript
是用C99
(C的一种标准)实现的。
First of all
在app的build.gradle文件加入下面两行:
defaultConfig {
applicationId "com.uniquestudio.renderscript"
minSdkVersion 14
targetSdkVersion 23
versionCode 1
versionName "1.0"
renderscriptTargetApi 18
renderscriptSupportModeEnabled true
}
考虑到兼容性,在使用RenderScript
的类中,要引入:import android.support.v8.renderscript.*;
从实用性出发,下面介绍三种处理图片的方式。
Blur Image
现在很多APP都在使用 模糊 效果的图片,RenderScript
提供了ScriptIntrinsicBlur
帮助我们实现模糊效果。一个渲染的过程其实就是一个加工的过程,把送进来的原材料加工成想要的产品。
看一下这个加工过程:
/**
*
* @param bitmap src
* @param radius the radius of blur ,max is 25
* @param context
* @return a blur bitmap
*/
public static Bitmap blurBitmap(Bitmap bitmap, float radius, Context context) {
//Create renderscript
RenderScript rs = RenderScript.create(context);
//Create allocation from Bitmap
Allocation allocation = Allocation.createFromBitmap(rs, bitmap);
Type t = allocation.getType();
//Create allocation with the same type
Allocation blurredAllocation = Allocation.createTyped(rs, t);
//Create blur script
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
//Set blur radius (maximum 25.0)
blurScript.setRadius(radius);
//Set input for script
blurScript.setInput(allocation);
//Call script for output allocation
blurScript.forEach(blurredAllocation);
//Copy script result into bitmap
blurredAllocation.copyTo(bitmap);
//Destroy everything to free memory
allocation.destroy();
blurredAllocation.destroy();
blurScript.destroy();
t.destroy();
rs.destroy();
return bitmap;
}
参数radius
是模糊程度。虽然我们没有自己写脚本,但是抛开脚本,从上层java代码我们不难发现,使用RenderScript
的几个关键步骤:
- 将数据填充在Allocation对象中,一般来说需要两个Allocation,一个存放,原材料另一个是作为加工之后的数据存放地
- 开启渲染
- 将Allocation中的产品输出
- 回收资源
代码很简单,不需要太多解释。在模糊效率上,是用java实现的fastblur
速度的8倍,而且在ORM的问题上也有优化。
效果图:
blur
Sketch Image
实现素描效果,处理的算法很简单:
求RGB平均值Avg = (R + G + B) / 3,如果Avg >= 100,则新的颜色值为R=G=B=255;如果Avg < 100,则新的颜色值为R=G=B=0;255就是白色,0就是黑色;至于为什么用100作比较,这是一个经验值吧,设置为128也可以,可以根据效果来调整。
STEP1:Writing a RenderScript Kernel
首先从渲染脚本文件写起,在main文件下新建一个包res,在res中新建sketch.rs
注意文件的后缀是rs
我们先看一下官方文档的写法:
A simple kernel may look like the following:
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;
}
文档对此的解释:
- The first notable feature is the attribute((kernel)) applied to the function prototype. This denotes that the function is a RenderScript kernel instead of an invokable function.(attribute((kernel))用来区分kernel和invokable方法)
- 函数参数的x,y,或者z是可选的,但是类型必须是uint32_t
-
pragma version(1)
是指版本号,目前只能选择1,没有更高的版本了。 -
pragma rs java_package_name(com.hc.renderscript)
是告诉编译器,包的名称。因为每个rs文件都会自动生成对应的Java代码。
我对in
参数的理解是allocation中的Element
#pragma version(1)
#pragma rs_fp_relaxed
#pragma rs java_package_name(com.uniquestudio.renderscript)
// Debugging RenderScript
#include "rs_debug.rsh"
uchar4 __attribute__((kernel)) invert(uchar4 in, uint32_t x, uint32_t y) {
//Convert input uchar4 to float4
float4 f4 = rsUnpackColor8888(in);
float r = f4.r;
float g = f4.g;
float b = f4.b;
rsDebug("Red", r);
if((r+g+b)*255/3>=100){
return rsPackColorTo8888(1, 1, 1, f4.a);
}else{
return rsPackColorTo8888(0, 0, 0, f4.a);
}
}
Tip:
#include "rs_debug.rsh"
可以打印log,但是在kernel method中打印log会造成io操作,使得整个处理过程的时间变长。
STEP2:Build project to generate .classes file
build之后会生成ScriptC_sketch
java文件,格式固定ScriptC_xxx,以此类推。
STEP3:Call scripts from Java code
在RenderScript
中用java代码控制脚本的生命周期。
public class SketchUtil {
public static Bitmap sketchBitmap(Bitmap bitmap,Context context){
RenderScript renderScript = RenderScript.create(context);
ScriptC_sketch sketchScript = new ScriptC_sketch(renderScript);
Allocation in = Allocation.createFromBitmap(renderScript,bitmap);
Allocation out = Allocation.createTyped(renderScript,in.getType());
// call kernel
sketchScript.forEach_invert(in,out);
out.copyTo(bitmap);
renderScript.destroy();
sketchScript.destroy();
in.destroy();
out.destroy();
return bitmap;
}
}
最后的效果图:
sketch
是不是很简单?之前我在kernel方法中打印过log,我发现,在java层中调用forEach_xx会遍历传入的Allocation参数,那么kernel方法中不要自己写循环了。
Magnifier Image
实现放大镜效果,类似这种:
example
假如我们定义放大镜的坐标为(atX,atY),半径为radius,而放大倍数为scale,那么其实就是将原图中的坐标为(atX,atY)、半径为radius/scale的区域的图像放大到放大镜覆盖的区域即可,算法其实很简单,对图片上的每一个点(X,Y),求其与(atX,atY)的距离Distance,若Distance < Radius,则取原图中坐标为(X/scale,Y/scale)的像素的颜色值作为新的颜色值。
这个问题的难点在于如何取回(X/scale,Y/scale)的像素,我们必然要存储所有的像素,以便我们取回。这里需要使用到脚本文件中与Allocation
对应的rs_allocation
以及rsGetElementAt_uchar4(allocation, X, Y)
函数
具体的流程和上面的Sketch相同。源代码:
// Needed directive for RS to work
#pragma version(1)
// The java_package_name directive needs to use your Activity's package path
#pragma rs java_package_name(com.uniquestudio.renderscript)
// Store the input allocation
rs_allocation inputAllocation;
// Magnifying
// TODO: here, some checks should be performed to prevent atX and atY to be < 0, as well
// as them to not be greater than width and height
int atX;
int atY;
float radius;
float scale; // The scale is >= 1
uchar4 __attribute__((kernel)) magnify(uchar4 in, int x, int y) {
// Calculates the distance between the touched point and the current kernel
// iteration pixel coordinated
// Reference: http://math.stackexchange.com/a/198769
float pointDistanceFromCircleCenter = sqrt(pow((float)(x - atX),2) + pow((float)(y - atY),2));
// Is this pixel outside the magnify radius?
if(radius < pointDistanceFromCircleCenter)
{
// In this case, just copy the original image
return in;
}
// If the point is inside the magnifying inner radius, draw the magnified image
// Calculates the current distance from the chosen magnifying center
float diffX = x - atX;
float diffY = y - atY;
// Scales down the distance accordingly to scale and returns the original coordinates
int originalX = atX + round(diffX / scale);
int originalY = atY + round(diffY / scale);
// Return the original image pixel at the calculated coordinates
return rsGetElementAt_uchar4(inputAllocation, originalX, originalY);
}
Java Code:
public static Bitmap magnifierBitmap(Bitmap bitmap, int x, int y, int radius,int scale, Context context){
RenderScript rs = RenderScript.create(context);
Allocation in = Allocation.createFromBitmap(rs, bitmap);
Allocation out = Allocation.createTyped(rs,in.getType());
ScriptC_magnifier magnifier = new ScriptC_magnifier(rs);
magnifier.set_inputAllocation(in);
magnifier.set_atX(x);
magnifier.set_atY(y);
magnifier.set_radius(radius);
magnifier.set_scale(scale);
magnifier.forEach_magnify(in,out);
out.copyTo(bitmap);
rs.destroy();
magnifier.destroy();
in.destroy();
out.destroy();
return bitmap;
}
效果图:
Magnifier
Advanced RenderScript
这一部分介绍两个layer(runtime和reflected),了解关于renderscript编译和执行。
RenderScript Runtime Layer
RenderScript
的编译和执行都发生在这个层。
编译的过程:共进行两次编译。
- 第一次
.rs
文件被llvm compiler
编译为字节码, - 第二次将字节码由设备上的
llvm compiler
编译为机器码,而且机器码会在设备上存储起来,这样之后RenderScript
的执行就不再需要编译字节码了。
这样就不难解释为什么RenderScript的移植性比较高了。
在RenderScript
运行的库中有几个关键的功能:
- 内存分配功能
- 大量的数学计算函数
- 数据类型的转化
- log函数
Reflected Layer
为了能从Android Framwork
层访问RenderScript Runtime
层,Android Build Tools生成了反射层。
.rs
脚本文件被反射成位于project_root/gen/package/name/ScriptC_*renderscript_filename*
的类,也就是.rs
的java版本文件,然后我们就能从Android Framework层调用了。
在.rs
文件的反射中,无非是要把文件的变量和函数进行反射。
Variables
如果在RenderScript
中有以下声明:
uint32_t unsignedInteger = 1;
那么反射之后会产生:
private long mExportVar_unsignedInteger;
public void set_unsignedInteger(long v){
mExportVar_unsignedInteger = v;
setVar(mExportVarIdx_unsignedInteger, v);
}
public long get_unsignedInteger(){
return mExportVar_unsignedInteger;
}
所以我们在Java层就可以使用get或者set对RenderScript
中的变量进行操作了。
但是如果在RenderScript
中有const
修饰变量时,就会不会产生set方法。
Functions
如果在RenderScript
中定义了这样一个函数:
void touch(float x, float y, float pressure, int id) {
if (id >= 10) {
return;
}
touchPos[id].x = x;
touchPos[id].y = y;
touchPressure[id] = pressure;
}
反射之后产生:
public void invoke_touch(float x, float y, float pressure, int id) {
FieldPacker touch_fp = new FieldPacker(16);
touch_fp.addF32(x);
touch_fp.addF32(y);
touch_fp.addF32(pressure);
touch_fp.addI32(id);
invoke(mExportFuncIdx_touch, touch_fp);
}
在RenderScript
中函数最好不要带返回值,RenderScript
本身就是异步的,如果函数有返回值,那么Android Framework调用时就会一直阻塞,直到有值返回,这样在处理大量计算任务的时候会直接影响效率。
上面的代码已经上传到github,传送门
网友评论
这么解释差不多,可以把allocation看作容器,Type看作链表(比如List),Element看作链表中的数据(比如int)。kernel函数就是并行的将链表中的数据一个一个的映射成in然后进行计算。再将结果out映射到输出容器进行返回。
res是资源文件夹