java JNI编程指南 下载本文

Get/SetArrayRegion系列函数。这些函数会进行越界检查,在需要的时候会有可能抛出ArrayIndexOutOfBoundsException异常。

对于少量的、固定大小的数组,Get/SetArrayRegion是最好的选择,因为C缓冲区可以在Stack(栈)上被很快地分配,而且复制少量数组元素的代价是很小的。这对函数的另外一个优点就是,允许你通过传入一个索引和长度来实现对子字符串的操作。

如果你没有一个预先分配的C缓冲区,并且原始数组长度未定,而本地代码又不想在获取数组元素的指针时阻塞的话,使用Get/ReleasePrimitiveArrayCritical函数对。就像Get/ReleaseStringCritical函数对一样,这对函数很小心地使用,以避免死锁。

Get/ReleaseArrayElements系列函数永远是安全的。JVM会选择性地返回一个指针,这个指针可能指向原始数据也可能指向原始数据复制。

3.3.5 访问对象数组

JNI提供了一个函数对来访问对象数组。GetObjectArrayElement返回数组中指定位置的元素,而SetObjectArrayElement修改数组中指定位置的元素。与基本类型的数组不同的是,你不能一次得到所有的对象元素或者一次复制多个对象元素。字符串和数组都是引用类型,你要使用Get/SetObjectArrayElement来访问字符串数组或者数组的数组。

下面的例子调用了一个本地方法来创建一个二维的int数组,然后打印这个数组的内容:

class ObjectArrayTest {

private static native int[][] initInt2DArray(int size); public static void main(String[] args) { int[][] i2arr = initInt2DArray(3); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) {

System.out.print(\ }

System.out.println(); } }

static {

System.loadLibrary(\ } }

静态本地方法initInt2DArray创建了一个给定大小的二维数组。执行分配和初始化数组任务的本地方法可以是下面这样子的:

JNIEXPORT jobjectArray JNICALL

Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size) {

jobjectArray result; int i;

jclass intArrCls = (*env)->FindClass(env, \ if (intArrCls == NULL) {

return NULL; /* exception thrown */ }

result = (*env)->NewObjectArray(env, size, intArrCls, NULL); if (result == NULL) {

return NULL; /* out of memory error thrown */ }

for (i = 0; i < size; i++) {

jint tmp[256]; /* make sure it is large enough! */ int j;

jintArray iarr = (*env)->NewIntArray(env, size); if (iarr == NULL) {

return NULL; /* out of memory error thrown */ }

for (j = 0; j < size; j++) { tmp[j] = i + j; }

(*env)->SetIntArrayRegion(env, iarr, 0, size, tmp); (*env)->SetObjectArrayElement(env, result, i, iarr); (*env)->DeleteLocalRef(env, iarr); }

return result; }

函数newInt2DArray首先调用JNI函数FindClass来获得一个int型二维数组类的引用,传递给FindClass的参数“[I”是JNI class descriptor(JNI类型描述符),它对应着JVM中的int[]类型。如果类加载失败的话,FindClass会返回NULL,然后抛出一个异常。

接下来,NewObjectArray会分配一个数组,这个数组里面的元素类型用intArrCls类引用来标识。函数NewObjectArray只能分配第一维,JVM没有与多维数组相对应的数据结构。一个二维数组实际上就是一个简单的数组的数组。 创建第二维数据的方式非常直接,NewInt-Array为每个数组元素分配空间,然后SetIntArrayRegion把tmp[]缓冲区中的内容复制到新分配的一维数组中去。 在循环最后调用DeleteLocalRef,确保JVM释放掉iarr这个JNI引用。

第四章 字段和方法

现在,你知道了如何通过JNI来访问JVM中的基本类型数据和字符串、数组这样的引用类型数据,下一步就是学习怎么样和JVM中任意对象的字段和方法进行交互。比如从本地代码中调用JAVA中的方法,也就是通常说的来自本地方法中的callbacks(回调)。 我们从进行字段访问和方法回调时需要的JNI函数开始讲解。本章的稍后部分我们会讨论怎么样通过一些cache(缓存)技术来优化这些操作。在最后,我们还会讨论从本地代码中访问字段和回调方法时的效率问题。

4.1 访问字段

JAVA支持两种field(字段),每一个对象的实例都有一个对象字段的复制;所有的对象共享一个类的静态字段。本地方法使用JNI提供的函数可以获取和修改这两种字段。先看一个从本地代码中访问对象字段的例子:

class InstanceFieldAccess { private String s;

private native void accessField();

public static void main(String args[]) {

InstanceFieldAccess c = new InstanceFieldAccess(); c.s = \ c.accessField();

System.out.println(\

System.out.println(\ }

static {

System.loadLibrary(\ } }

InstanceFieldAccess这个类定义了一个对象字段s。main方法创建了一个对象并设置s的值,然后调用本地方法InstanceFieldAccess.accessField在本地代码中打印s的值,并把它修改为一个新值。本地方法返回后,JAVA中把这个值再打印一次,可以看出来,字段s的值已经被改变了。下面是本地方法的实现: JNIEXPORT void JNICALL

Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj) {

jfieldID fid; /* store the field ID */ jstring jstr; const char *str;

/* Get a reference to obj's class */

jclass cls = (*env)->GetObjectClass(env, obj);

printf(\

/* Look for the instance field s in cls */ fid = (*env)->GetFieldID(env, cls, \

\ if (fid == NULL) {

return; /* failed to find the field */ }

/* Read the instance field s */

jstr = (*env)->GetObjectField(env, obj, fid); str = (*env)->GetStringUTFChars(env, jstr, NULL); if (str == NULL) {

return; /* out of memory */ }

printf(\

(*env)->ReleaseStringUTFChars(env, jstr, str);

/* Create a new string and overwrite the instance field */ jstr = (*env)->NewStringUTF(env, \ if (jstr == NULL) {

return; /* out of memory */ }

(*env)->SetObjectField(env, obj, fid, jstr); }

运行程序,得到输出为:

In C:

c.s = \ In Java:

c.s = \

4.1.1 访问一个对象字段的流程

为了访问一个对象的实例字段,本地方法需要做两步:

首先,通过在类引用上调用GetFieldID获取field ID(字段ID)、字段名字和字段描述符: Fid=(*env)->GetFieldID(env,cls,”s”,”Ljava/lang/String;”);

上例中的代码通过在对象引用obj上调用GetObjectClass获取到类引用。一旦获取到字段ID,你就可以把对象和字段ID作为参数来访问字段: Jstr=(*env)->GetObjectField(env,obj,fid);

因为字符串和数组是特殊的对象,所以我们使用GetObjectField来访问字符串类型的实例字段。除了Get/SetObjectField,JNI还支持其它如GetIntField、SetFloatField等用来访问基本类型字段的函数。

4.1.2 字段描述符

在上一节我们使用过一个特殊的C字符串“Ljava/lang/String”来代表一个JVM中的字段类型。这个字符串被称为JNI field descriptor(字段描述符)。

字符串的内容由字段被声明的类型决定。例如,使用“I”来表示一个int类型的字段,“F”来表示一个float类型的字段,“D”来表示一个double类型的字段,“Z”来表示一个boolean类型的字段等等。

像java.lang.String这样的引用类型的描述符都是以L开头,后面跟着一个JNI类描述符,以分号结尾。一个JAVA类的全名中的包名分隔符“.”被转化成“/”。因此,对于一个字段类

型的字段来说,它的描述符是“Ljava/lang/String”。

数组的描述符中包含“]”字符,后面会跟着数组类型的描述符,如“[I”是int[]类型的字段的描述符。12.3.3详细介绍了各种类型的字段描述以及他们代表的JAVA类型。 你可以使用javap工具来生成字段描述符。

4.1.3 访问静态字段

访问静态字段和访问实例字段相似,看下面这个InstanceFieldAccess例子的变形:

class StaticFielcdAccess { private static int si;

private native void accessField();

public static void main(String args[]) {

StaticFieldAccess c = new StaticFieldAccess(); StaticFieldAccess.si = 100; c.accessField();

System.out.println(\

System.out.println(\ }

static {

System.loadLibrary(\ } }

StaticFieldAccess这个类包含一个静态字段si,main方法创建了一个对象,初始化静态字段,然后调用本地方法StaticFieldAccess.accessField在本地代码中打印静态字段中的值,然后设置新的值,为了演示这个值确实被改变了,在本地方法返回后,JAVA中再次这个静态字段的值。 下面是本地方法StaticFieldAccess.accessField的实现: JNIEXPORT void JNICALL

Java_StaticFieldAccess_accessField(JNIEnv *env, jobject obj) {

jfieldID fid; /* store the field ID */ jint si;

/* Get a reference to obj's class */

jclass cls = (*env)->GetObjectClass(env, obj);

printf(\

/* Look for the static field si in cls */

fid = (*env)->GetStaticFieldID(env, cls, \ if (fid == NULL) {

return; /* field not found */ }