由于利用 RenderScript 的应用程序仍在 Android VM 中运行,因此您可以访问所有熟悉的框架 API,但在适当的时候可以利用 RenderScript。为了促进框架与 RenderScript 运行时之间的这种交互,还存在一个代码中间层,以促进这两个层级代码之间的通信和内存管理。本文档详细介绍了这些不同的代码层,以及内存如何在 Android VM 和 RenderScript 运行时之间共享。
RenderScript 运行时层
您的 RenderScript 代码在一个紧凑且定义良好的运行时层中进行编译和执行。RenderScript 运行时 API 为密集型计算提供支持,这些计算具有可移植性,并可根据处理器上可用的核心数量自动扩展。
注意:NDK 中的标准 C 函数必须保证在 CPU 上运行,因此 RenderScript 无法访问这些库,因为 RenderScript 旨在在不同类型的处理器上运行。
您在 Android 项目的 src/ 目录中的 .rs 和 .rsh 文件中定义 RenderScript 代码。代码由作为 Android 构建一部分运行的 llvm 编译器编译为中间字节码。当您的应用程序在设备上运行时,字节码随后由驻留在设备上的另一个 llvm 编译器(即时地)编译为机器代码。机器代码针对设备进行了优化并被缓存,因此后续使用启用 RenderScript 的应用程序时不会重新编译字节码。
RenderScript 运行时库的一些关键特性包括
- 内存分配请求功能
- 大量的数学函数集合,包含许多常见例程的标量和向量类型的重载版本。提供加、乘、点积和叉积等运算,以及原子算术和比较函数。
- 原始数据类型和向量的转换例程、矩阵例程、以及日期和时间例程
- 支持 RenderScript 系统的数据类型和结构,例如用于定义二、三或四元向量的 Vector 类型。
- 日志记录函数
有关可用函数的更多信息,请参阅 RenderScript 运行时 API 参考。
反射层
反射层是 Android 构建工具生成的一组类,允许从 Android 框架访问 RenderScript 运行时。该层还提供了一些方法和构造函数,允许您为 RenderScript 代码中定义的指针分配内存并进行操作。以下列表描述了反射出的主要组件
- 您创建的每个
.rs文件都会生成到名为project_root/gen/package/name/ScriptC_renderscript_filename的ScriptC类型类中。该文件是您的.rs文件的.java版本,您可以从 Android 框架中调用它。此类包含以下从.rs文件反射出的项- 非静态函数
- 非静态、全局 RenderScript 变量。会为每个变量生成访问器方法,因此您可以从 Android 框架读取和写入 RenderScript 变量。如果全局变量在 RenderScript 运行时层初始化,则这些值将用于初始化 Android 框架层中的相应值。如果全局变量被标记为
const,则不会生成set方法。更多详细信息请看这里。 - 全局指针
struct会反射到它自己名为project_root/gen/package/name/ScriptField_struct_name的类中,该类扩展了Script.FieldBase。此类代表struct的数组,它允许您为一个或多个此struct的实例分配内存。
函数
函数会被反射到脚本类本身中,位于 project_root/gen/package/name/ScriptC_renderscript_filename。例如,如果您在 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; }
那么将生成以下 Java 代码
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 系统设计为异步的。当您的 Android 框架代码调用 RenderScript 时,调用会被放入队列并尽可能执行。这种限制允许 RenderScript 系统在没有持续中断的情况下运行,提高了效率。如果允许函数具有返回值,则调用将阻塞直到返回值返回。
如果您希望 RenderScript 代码向 Android 框架发送回值,请使用 rsSendToClient() 函数。
变量
支持类型的变量会被反射到脚本类本身中,位于 project_root/gen/package/name/ScriptC_renderscript_filename。会为每个变量生成一组访问器方法。例如,如果您在 RenderScript 代码中定义了以下变量
uint32_t unsignedInteger = 1;
那么将生成以下 Java 代码
private long mExportVar_unsignedInteger; public void set_unsignedInteger(long v){ mExportVar_unsignedInteger = v; setVar(mExportVarIdx_unsignedInteger, v); } public long get_unsignedInteger(){ return mExportVar_unsignedInteger; }
结构体 (Structs)
结构体会被反射到它们各自的类中,位于 <project_root>/gen/com/example/renderscript/ScriptField_struct_name。此类代表 struct 的数组,并允许您为指定数量的 struct 分配内存。例如,如果您定义了以下结构体
typedef struct Point { float2 position; float size; } Point_t;
那么将在 ScriptField_Point.java 中生成以下代码
package com.example.android.rs.hellocompute; import android.renderscript.*; import android.content.res.Resources; /** * @hide */ public class ScriptField_Point extends android.renderscript.Script.FieldBase { static public class Item { public static final int sizeof = 12; Float2 position; float size; Item() { position = new Float2(); } } private Item mItemArray[]; private FieldPacker mIOBuffer; public static Element createElement(RenderScript rs) { Element.Builder eb = new Element.Builder(rs); eb.add(Element.F32_2(rs), "position"); eb.add(Element.F32(rs), "size"); return eb.create(); } public ScriptField_Point(RenderScript rs, int count) { mItemArray = null; mIOBuffer = null; mElement = createElement(rs); init(rs, count); } public ScriptField_Point(RenderScript rs, int count, int usages) { mItemArray = null; mIOBuffer = null; mElement = createElement(rs); init(rs, count, usages); } private void copyToArray(Item i, int index) { if (mIOBuffer == null) mIOBuffer = new FieldPacker(Item.sizeof * getType().getX()/* count */); mIOBuffer.reset(index * Item.sizeof); mIOBuffer.addF32(i.position); mIOBuffer.addF32(i.size); } public void set(Item i, int index, boolean copyNow) { if (mItemArray == null) mItemArray = new Item[getType().getX() /* count */]; mItemArray[index] = i; if (copyNow) { copyToArray(i, index); mAllocation.setFromFieldPacker(index, mIOBuffer); } } public Item get(int index) { if (mItemArray == null) return null; return mItemArray[index]; } public void set_position(int index, Float2 v, boolean copyNow) { if (mIOBuffer == null) mIOBuffer = new FieldPacker(Item.sizeof * getType().getX()/* count */); if (mItemArray == null) mItemArray = new Item[getType().getX() /* count */]; if (mItemArray[index] == null) mItemArray[index] = new Item(); mItemArray[index].position = v; if (copyNow) { mIOBuffer.reset(index * Item.sizeof); mIOBuffer.addF32(v); FieldPacker fp = new FieldPacker(8); fp.addF32(v); mAllocation.setFromFieldPacker(index, 0, fp); } } public void set_size(int index, float v, boolean copyNow) { if (mIOBuffer == null) mIOBuffer = new FieldPacker(Item.sizeof * getType().getX()/* count */); if (mItemArray == null) mItemArray = new Item[getType().getX() /* count */]; if (mItemArray[index] == null) mItemArray[index] = new Item(); mItemArray[index].size = v; if (copyNow) { mIOBuffer.reset(index * Item.sizeof + 8); mIOBuffer.addF32(v); FieldPacker fp = new FieldPacker(4); fp.addF32(v); mAllocation.setFromFieldPacker(index, 1, fp); } } public Float2 get_position(int index) { if (mItemArray == null) return null; return mItemArray[index].position; } public float get_size(int index) { if (mItemArray == null) return 0; return mItemArray[index].size; } public void copyAll() { for (int ct = 0; ct < mItemArray.length; ct++) copyToArray(mItemArray[ct], ct); mAllocation.setFromFieldPacker(0, mIOBuffer); } public void resize(int newSize) { if (mItemArray != null) { int oldSize = mItemArray.length; int copySize = Math.min(oldSize, newSize); if (newSize == oldSize) return; Item ni[] = new Item[newSize]; System.arraycopy(mItemArray, 0, ni, 0, copySize); mItemArray = ni; } mAllocation.resize(newSize); if (mIOBuffer != null) mIOBuffer = new FieldPacker(Item.sizeof * getType().getX()/* count */); } }
生成的代码是为了方便您为 RenderScript 运行时请求的结构体分配内存,并与内存中的 struct 进行交互。每个 struct 的类定义了以下方法和构造函数
- 允许您分配内存的重载构造函数。
ScriptField_struct_name(RenderScript rs, int count)构造函数允许您通过count参数定义要为其分配内存的结构体数量。ScriptField_struct_name(RenderScript rs, int count, int usages)构造函数定义了一个额外的参数usages,让您指定此内存分配的内存空间。有四种可能的内存空间USAGE_SCRIPT:在脚本内存空间中分配。如果您未指定内存空间,这是默认的内存空间。USAGE_GRAPHICS_TEXTURE:在 GPU 的纹理内存空间中分配。USAGE_GRAPHICS_VERTEX:在 GPU 的顶点内存空间中分配。USAGE_GRAPHICS_CONSTANTS:在各种程序对象使用的 GPU 常量内存空间中分配。
您可以使用按位
OR运算符指定多个内存空间。这样做会通知 RenderScript 运行时您打算访问指定内存空间中的数据。以下示例在脚本和顶点内存空间中为自定义数据类型分配内存Kotlin
val touchPoints: ScriptField_Point = ScriptField_Point( myRenderScript, 2, Allocation.USAGE_SCRIPT or Allocation.USAGE_GRAPHICS_VERTEX )
Java
ScriptField_Point touchPoints = new ScriptField_Point(myRenderScript, 2, Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_VERTEX);
- 一个静态嵌套类
Item,允许您以对象的形式创建struct的实例。如果您觉得在 Android 代码中使用该struct更有意义,这个嵌套类会很有用。操作完对象后,您可以通过调用set(Item i, int index, boolean copyNow)并将Item设置在数组中的期望位置,将对象推送到分配的内存。RenderScript 运行时会自动获得对新写入内存的访问权限。 - 用于获取和设置结构体中每个字段值的访问器方法。这些访问器方法中的每一个都有一个
index参数,用于指定您要读取或写入的数组中的struct。每个 setter 方法还有一个copyNow参数,用于指定是否立即将此内存同步到 RenderScript 运行时。要同步尚未同步的任何内存,请调用copyAll()。 createElement()方法在内存中创建该结构体的描述。此描述用于分配由一个或多个元素组成的内存。resize()的工作原理非常类似于 C 中的realloc(),允许您扩展以前分配的内存,同时保留之前创建的当前值。copyAll()将框架级别设置的内存同步到 RenderScript 运行时。当您在成员上调用 set 访问器方法时,可以指定一个可选的copyNow布尔参数。指定为true会在您调用该方法时同步内存。如果您指定为 false,则可以调用一次copyAll(),它会为所有尚未同步的属性同步内存。
指针
全局指针会被反射到脚本类本身中,位于 project_root/gen/package/name/ScriptC_renderscript_filename。您可以声明指向 struct 或任何受支持的 RenderScript 类型的指针,但 struct 不能包含指针或嵌套数组。例如,如果您定义了以下指向 struct 和 int32_t 的指针
typedef struct Point { float2 position; float size; } Point_t; Point_t *touchPoints; int32_t *intPointer;
那么将生成以下 Java 代码
private ScriptField_Point mExportVar_touchPoints; public void bind_touchPoints(ScriptField_Point v) { mExportVar_touchPoints = v; if (v == null) bindAllocation(null, mExportVarIdx_touchPoints); else bindAllocation(v.getAllocation(), mExportVarIdx_touchPoints); } public ScriptField_Point get_touchPoints() { return mExportVar_touchPoints; } private Allocation mExportVar_intPointer; public void bind_intPointer(Allocation v) { mExportVar_intPointer = v; if (v == null) bindAllocation(null, mExportVarIdx_intPointer); else bindAllocation(v, mExportVarIdx_intPointer); } public Allocation get_intPointer() { return mExportVar_intPointer; }
会生成一个 get 方法和一个名为 bind_pointer_name 的特殊方法(而不是 set() 方法)。bind_pointer_name 方法允许您将 Android VM 中分配的内存绑定到 RenderScript 运行时(您不能在 .rs 文件中分配内存)。欲了解更多信息,请参阅操作已分配的内存。
内存分配 API
使用 RenderScript 的应用程序仍在 Android VM 中运行。然而,实际的 RenderScript 代码以原生方式运行,并需要访问 Android VM 中分配的内存。为了实现这一点,您必须将 VM 中分配的内存附加到 RenderScript 运行时。这个过程称为绑定,它允许 RenderScript 运行时无缝地处理它请求但无法显式分配的内存。最终结果本质上与您在 C 中调用 malloc 相同。额外的好处是 Android VM 可以执行垃圾回收,并与 RenderScript 运行时层共享内存。绑定仅对动态分配的内存是必要的。静态分配的内存会在编译时自动为您的 RenderScript 代码创建。有关内存分配如何发生的更多信息,请参见图 1。
为了支持这个内存分配系统,有一组 API 允许 Android VM 分配内存,并提供类似于 malloc 调用的功能。这些类本质上描述了应该如何分配内存,并执行分配。为了更好地理解这些类是如何工作的,可以将它们与简单的 malloc 调用联系起来思考,就像这样:
array = (int *)malloc(sizeof(int)*10);
malloc 调用可以分为两部分:被分配的内存大小(sizeof(int)),以及应该分配多少个单位的内存(10)。Android 框架为这两部分提供了类,以及一个代表 malloc 本身的类。
Element 类代表 malloc 调用的 (sizeof(int)) 部分,并封装了内存分配的一个单元,例如单个 float 值或一个结构体。Type 类封装了 Element 和要分配的元素数量(在我们的示例中为 10)。您可以将 Type 看作是 Element 的数组。Allocation 类基于给定的 Type 执行实际的内存分配,并代表实际分配的内存。
在大多数情况下,您不需要直接调用这些内存分配 API。反射层类会自动生成使用这些 API 的代码,分配内存所要做的就是调用反射层类之一中声明的构造函数,然后将生成的内存 Allocation 绑定到 RenderScript。在某些情况下,您需要直接使用这些类来自己分配内存,例如从资源中加载位图,或者当您想为原始类型的指针分配内存时。您可以在为 RenderScript 分配和绑定内存部分了解如何操作。下表详细描述了这三个内存管理类
| Android 对象类型 | 描述 |
|---|---|
Element |
一个元素 (Element) 描述了内存分配的一个单元,可以有两种形式:基本或复杂。 基本元素包含任何有效 RenderScript 数据类型的单个数据组件。基本元素数据类型的示例包括单个 复杂元素包含一组基本元素,并由您在 RenderScript 代码中声明的 |
类型 |
类型 (Type) 是一种内存分配模板,由一个元素和一个或多个维度组成。它描述了内存的布局(基本上是 类型由五个维度组成:X、Y、Z、LOD(细节层次)和 Faces(立方体贴图的)。在可用内存的限制内,您可以将 X、Y、Z 维度设置为任何正整数值。一维分配的 X 维度大于零,而 Y 和 Z 维度为零表示不存在。例如,x=10, y=1 的分配被视为二维,而 x=10, y=0 被视为一维。LOD 和 Faces 维度是布尔值,用于指示存在或不存在。 |
Allocation |
内存分配 (Allocation) 根据 上传 Allocation 数据主要有两种方式:类型检查和非类型检查。对于简单的数组,有 |
操作内存
您在 RenderScript 中声明的非静态全局变量在编译时分配内存。您可以直接在 RenderScript 代码中使用这些变量,而无需在 Android 框架级别为它们分配内存。Android 框架层也可以通过反射层类中生成的访问器方法访问这些变量。如果这些变量在 RenderScript 运行时层被初始化,则这些值将用于初始化 Android 框架层中的相应值。如果全局变量标记为 const,则不会生成 set 方法。更多详细信息请看这里。
注意:如果您使用包含指针的某些 RenderScript 结构,例如 rs_program_fragment 和 rs_allocation,则必须首先获得相应 Android 框架类的对象,然后调用该结构的 set 方法将内存绑定到 RenderScript 运行时。您不能在 RenderScript 运行时层直接操作这些结构。此限制不适用于包含指针的用户定义结构,因为它们首先就无法导出到反射层类。如果您尝试声明一个包含指针的非静态全局结构体,则会产生编译器错误。
RenderScript 也支持指针,但您必须在 Android 框架代码中显式分配内存。当您在 .rs 文件中声明全局指针时,您通过相应的反射层类分配内存,并将该内存绑定到原生 RenderScript 层。您可以从 Android 框架层以及 RenderScript 层与该内存进行交互,这为您提供了在最合适的层修改变量的灵活性。
为 RenderScript 分配和绑定动态内存
要分配动态内存,您需要调用 Script.FieldBase 类的构造函数,这是最常用的方式。另一种方法是手动创建一个 Allocation,这对于原始类型指针等事物是必需的。为了简便起见,只要可行,就应使用 Script.FieldBase 类构造函数。获取内存分配后,调用该指针反射出的 bind 方法,将分配的内存绑定到 RenderScript 运行时。
下面的示例为原始类型指针 intPointer 和结构体指针 touchPoints 分配内存。它还将内存绑定到 RenderScript
Kotlin
private lateinit var myRenderScript: RenderScript private lateinit var script: ScriptC_example private lateinit var resources: Resources public fun init(rs: RenderScript, res: Resources) { myRenderScript = rs resources = res // allocate memory for the struct pointer, calling the constructor val touchPoints = ScriptField_Point(myRenderScript, 2) // Create an element manually and allocate memory for the int pointer val intPointer: Allocation = Allocation.createSized(myRenderScript, Element.I32(myRenderScript), 2) // create an instance of the RenderScript, pointing it to the bytecode resource script = ScriptC_point(myRenderScript/*, resources, R.raw.example*/) // bind the struct and int pointers to the RenderScript script.bind_touchPoints(touchPoints) script.bind_intPointer(intPointer) ... }
Java
private RenderScript myRenderScript; private ScriptC_example script; private Resources resources; public void init(RenderScript rs, Resources res) { myRenderScript = rs; resources = res; // allocate memory for the struct pointer, calling the constructor ScriptField_Point touchPoints = new ScriptField_Point(myRenderScript, 2); // Create an element manually and allocate memory for the int pointer intPointer = Allocation.createSized(myRenderScript, Element.I32(myRenderScript), 2); // create an instance of the RenderScript, pointing it to the bytecode resource script = new ScriptC_example(myRenderScript, resources, R.raw.example); // bind the struct and int pointers to the RenderScript script.bind_touchPoints(touchPoints); script.bind_intPointer(intPointer); ... }
读写内存
您可以在 RenderScript 运行时和 Android 框架层对静态和动态分配的内存进行读写。
静态分配的内存在 RenderScript 运行时级别具有单向通信限制。当 RenderScript 代码更改变量的值时,出于效率目的,它不会传回 Android 框架层。在调用 get 方法期间始终返回从 Android 框架设置的最后一个值。但是,当 Android 框架代码修改变量时,该更改可以自动传送到 RenderScript 运行时,或者稍后同步。如果您需要从 RenderScript 运行时向 Android 框架层发送数据,可以使用 rsSendToClient() 函数来克服此限制。
处理动态分配的内存时,如果您使用关联的指针修改了内存分配,则 RenderScript 运行时层的任何更改都会传播回 Android 框架层。在 Android 框架层修改对象会立即将该更改传播回 RenderScript 运行时层。
读写全局变量
读写全局变量是一个简单的过程。您可以在 Android 框架级别使用访问器方法,或者直接在 RenderScript 代码中设置它们。请记住,您在 RenderScript 代码中所做的任何更改都不会传播回 Android 框架层(更多详细信息请看这里)。
例如,假设在名为 rsfile.rs 的文件中声明了以下结构体
typedef struct Point { int x; int y; } Point_t; Point_t point;
您可以像这样直接在 rsfile.rs 中为结构体赋值。这些值不会传回 Android 框架级别
point.x = 1; point.y = 1;
您可以像这样在 Android 框架层为结构体赋值。这些值将异步传播回 RenderScript 运行时级别
Kotlin
val script: ScriptC_rsfile = ... ... script._point = ScriptField_Point.Item().apply { x = 1 y = 1 }
Java
ScriptC_rsfile script; ... Item i = new ScriptField_Point.Item(); i.x = 1; i.y = 1; script.set_point(i);
您可以像这样在 RenderScript 代码中读取值
rsDebug("Printing out a Point", point.x, point.y);
您可以使用以下代码在 Android 框架层读取值。请记住,此代码仅在 Android 框架级别设置了值时才返回值。如果您仅在 RenderScript 运行时级别设置了值,您将得到一个空指针异常
Kotlin
Log.i("TAGNAME", "Printing out a Point: ${mScript._point.x} ${mScript._point.y}") println("${point.x} ${point.y}")
Java
Log.i("TAGNAME", "Printing out a Point: " + script.get_point().x + " " + script.get_point().y); System.out.println(point.get_x() + " " + point.get_y());
读写全局指针
假设已经在 Android 框架级别分配了内存并绑定到了 RenderScript 运行时,您可以通过对该指针使用 get 和 set 方法在 Android 框架级别读写内存。在 RenderScript 运行时层中,您可以像平常一样使用指针读写内存,更改会传播回 Android 框架层,这与静态分配的内存不同。
例如,假设在名为 rsfile.rs 的文件中给出了以下指向 struct 的指针
typedef struct Point { int x; int y; } Point_t; Point_t *point;
假设您已经在 Android 框架层分配了内存,您可以照常访问 struct 中的值。您通过其指针变量对结构体所做的任何更改都自动可用于 Android 框架层
Kotlin
point[index].apply { x = 1 y = 1 }
Java
point[index].x = 1; point[index].y = 1;
您也可以在 Android 框架层读写该指针的值
Kotlin
val i = ScriptField_Point.Item().apply { x = 100 y = 100 } val p = ScriptField_Point(rs, 1).apply { set(i, 0, true) } script.bind_point(p) p.get_x(0) //read x and y from index 0 p.get_y(0)
Java
ScriptField_Point p = new ScriptField_Point(rs, 1); Item i = new ScriptField_Point.Item(); i.x=100; i.y = 100; p.set(i, 0, true); script.bind_point(p); p.get_x(0); //read x and y from index 0 p.get_y(0);
一旦内存已经绑定,每次对值进行更改时,都不需要重新将内存绑定到 RenderScript 运行时。