标签:
在使用JNI的时候,你问的最多的问题莫过于 Java的数据类型和C/C++的数据类型怎么一对一映射。在我们的HelloWord例子当中,我们并没有传入任何参数给我们的java层print方法,native方法也并没有返回任何数据而是void,本地方法只是简单的打印一个字符串,然后就返回了。实际开发中我们都需要传入参数,返回参数,本章就会讨论如何从java层向底层传数据,以及如何从底层向java层返回数据。我们从基本数据类型 字符串 数组开始, 下一章再介绍如何传任意类型的数据,以及如何访问他们的数据和方法。
我们来看一个不同于HelloWord的例子Prompt.java :等待用户输入,再把输入的字符串返回,源代码如下:
class Prompt { // native method that prints a prompt and reads a line private native String getLine(String prompt);//本地方法声明 public static void main(String args[]) { Prompt p = new Prompt(); String input = p.getLine("Type a line: ");//getLine方法返回用户输入的字符串 System.out.println("User typed: " + input); } static { System.loadLibrary("Prompt");//加载动态库 } }生成的Prompt.h头文件如下:
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);
*env 指向了一个函数表(JNI API),我们要和虚拟机交互,都需要调用JNI API才能完成, 并且env是一个二级指针
参数jobject this 有时候会不一样,如果你的native方法声明为static 则这个参数就代表Promt这个类,否则代表这个类的对象this
Java数据类型包括原生数据类型和引用数据类型:
原生数据类型int float double char 等
引用数据类型就是对象 String Array
JNI将这两类数据类型区别开来,原生数据类型-> C数据类型的映射是很直白的,很好理解,例如 int -> jint float-> jfloat
JNI将一个对象当做一个”不透明引用“ 传给底层方法 ,这个不透明引用其实是一个指针,指向虚拟机中的某个数据结构,这个数据结构对开发人员来说是不透明的。也就是说,这个数据结构的定义,你看不到,你只能通过一些JNI提供的API来和他们进行交互, 这就需要JNIEnv这个函数表了。举个例子java.lang.String 对应的JNI类型为jstring,jstring引用的字符串和我们的C代码是不相关的,明白了吗?我再解释一下:你可以把jstring想象成一个访问java.lang.String 对象的一个id,我们通过这个id能够访问到这个字符串,但是单从这个id来看,我们不知道这个字符串是什么,必须通过JNIEnv的函数才能访问到这个字符串。例如一些方法:GetStringUTFChars()
jobject 类型有一些子类型 比如 jstring(java String) jobjectArray (java对象数组), 其实这个java层是一个道理,这里简单提一下。
下面看一下如何访问字符串:
这个方法接受一个字符串参数jstring 然后返回一个jstring , jstring 和 char*还是有区别的,所以你不能把他们混淆使用,下面的代码执行会出错
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) { /* ERROR: incorrect use of jstring as a char* pointer */ printf("%s", prompt);//直接把jstring当成char*来使用是错误的 ... }
JNI支持Unicode UTF-8 和char* 之间来回转换的函数。Unicode是2个字节,UTF-8最高达到7个字节
GetStringUTFChars方法能够将Unicode串转换成UTF-8编码的C串,如果你确定你传入的是7bit的ASCII串,那么printf会正确输出,后面讲解怎么输出非ASCII串
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) { char buf[128]; const jbyte *str; str = (*env)->GetStringUTFChars(env, prompt, NULL); if (str == NULL) { return NULL; /* OutOfMemoryError already thrown */ } printf("%s", str); (*env)->ReleaseStringUTFChars(env, prompt, str); /* We assume here that the user does not type more than * 127 characters */ scanf("%s", buf); return (*env)->NewStringUTF(env, buf); }
别忘记调用ReleaseStringUTFChars,因为GetStringUTFChars 返回的指针是申请来的内存,保存我们的char*串,如果你不调用这个方法,就会产生内存泄漏。
Tip: 如果JNI方法返回的是指针,你就要考虑,是否有对应的方法来释放这个指针的内存,这或许是个好习惯。
new一个字符串:
你可以使用NewStringUTF方法来new一个java字符串,如果返回NULL说明没内存,会抛OutOfMemory异常,同上。如果成功则返回一个jstring类型的数据
你不需要担心jstring的释放问题,因为这是一个java层的String,释放的工作交给虚拟机GC吧。
其他JNI字符串函数:
GetStringChars 和 ReleaseStringChars 返回Unicode编码的C字符串,注意,是C串类型是UTF-8还是Unicode编码。
GetStringChars 有个参数isCopy ,如果设置成JNI_TRUE 那么会返回一份拷贝 JNI_FALSE的话就直接指向原始内存地址。当设置成JNI_FALSE时,你最好别去修改内存中的内容,这违背了java.lang.String 的设计原则:不能修改字符串。一般我们都传NULL,即拷贝字符串,如果传JNI_TRUE,也需要调用ReleaseStringChars,这就和GC相关了。
为了能够直接返回java层String的char*而不拷贝,java2 sdk release1.2引入了一对新的JNI API: Get/ReleaseStringCritical,表面看来和Get/ReleaseStringChars 很像,但是实际上它的使用限制还是很多的。你可以把get release之间的代码当成一个critical reigion,特殊的区域,在这个区域里,你不许再调用JNI函数,或者能够引起当前线程阻塞的函数,比如等待IO,这个限制使得虚拟机在这个区域里禁止垃圾回收机制,也就是说,当你拿到了一个指向java层string的native char* 时,垃圾回收不会运行,会被阻塞。
native code 在这个区域不能调用block阻塞函数,也不能new java对象,否则虚拟机可能就会死锁。考虑如下情形:另一个线程触发了垃圾回收,不能向下执行(直到当前线程不阻塞,并且跳出Get/ReleaseStringCritical之间的区域启动垃圾回收), 也就是说垃圾回收被阻塞了,与此同时,当前线程因为阻塞也不能向下执行了,且垃圾回收线程也被阻塞了,它需要等待另一个线程释放锁,且这个线程也等待执行垃圾回收,这样可能就会造成死锁。
但是你可以在这个特殊区域调用Get/ReleaseStringCritical, overlap,嵌套调用,比如如下代码片段:
jchar *s1, *s2; s1 = (*env)->GetStringCritical(env, jstr1); if (s1 == NULL) { ... /* error handling */ } s2 = (*env)->GetStringCritical(env, jstr2); if (s2 == NULL) { (*env)->ReleaseStringCritical(env, jstr1, s1); ... /* error handling */ } ... /* use s1 and s2 */ (*env)->ReleaseStringCritical(env, jstr1, s1); (*env)->ReleaseStringCritical(env, jstr2, s2);
可以被允许在这个特殊区域调用的函数有Get/ReleaseStringCritical 和Get/ReleasePrimitiveArrayCritical ,JNI不支持GetStringUTFCritical 和 ReleaseStringUTFCritical
Java 2 SDK release 1.2其他的函数GetStringRegion 和 GetStringUTFRegion ,这些函数会接受一个预分配的buffer作为参数,我们的getLine也可以这样实现:
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) { /* assume the prompt string and user input has less than 128 characters */ char outbuf[128], inbuf[128]; int len = (*env)->GetStringLength(env, prompt); (*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf); printf("%s", outbuf); scanf("%s", inbuf); return (*env)->NewStringUTF(env, inbuf); }
GetStringUTFRegion比GetStringUTFChars 要简单很多,因为GetStringUTFRegion并不会分配内存,我们也不用检查out-of-memory。
JNI 有关字符串的函数概要:
这么多函数,我们如何选择呢?
也就是说:如果你的目标版本是1.1或者同时1.1,1.2,你别无选择,只有Get/ReleaseStringChars and Get/ReleaseStringUTFChars
如果你的目标版本是1.2或1.2以上,并且你希望copy字符串到你预分配的数组里,请用GetStringRegion or GetStringUTFRegion.对于固定长度的或者比较短小的字符串,这两个函数是再合适不过的了,他们的优势是他们不需要分配内存,也就不会导致out-of-memory异常,访问子串使用本方法也是合适的选择。
GetStringCritical的使用就得小心了,你不能在特殊区域内阻塞,或者调用除了上面提到的方法外的任何JNI函数,否则,就会导致系统死锁。我们用一个例子演示一下:
/* This is not safe! */ const char *c_str = (*env)->GetStringCritical(env, j_str, 0); if (c_str == NULL) { ... /* error handling */ } fprintf(fd, "%s\n", c_str); (*env)->ReleaseStringCritical(env, j_str, c_str);这段代码不安全,是因为,当垃圾回收被禁用的时候,你向文件中写数据。假如,另一个线程T正在等待读fd,我们进一步假定fprintf会等待线程T从fd中读完数据。这种情形就会死锁:如果线程T没有申请到足够的内存,那么会触发垃圾回收,但是垃圾回收被禁用了,换句话说垃圾回收被阻塞了直到ReleaseStringCritical被调用,但是,只能等待fprintf返回才能调用,但是fprintf正在等待线程T执行读操作的结束,这不就死锁了吗。
下面的代码片段是安全的:
/* This code segment is OK. */ const char *c_str = (*env)->GetStringCritical(env, j_str, 0); if (c_str == NULL) { ... /* error handling */ } DrawString(c_str); (*env)->ReleaseStringCritical(env, j_str, c_str);
总之,在使用Get/ReleaseStringCritical的时候你一定要格外的小心,确保这个特殊区域的代码永远不会阻塞。(少做调用即可,除非你确定,你的调用一定不阻塞)
以上就是关于JNI操作字符串的一切,下面我们讨论下一个主题,访问数组
JNI对于原生数据类型的数组,和引用类型的数组的对待方式是不一样的,原生数据类型的数组包含原生类型的数据,例如int boolean, 引用类型的数组包含引用类型的数据,比如对象的实例和其他的数组(二维数组) 例如 如下java代码片段:
int[] iarr; float[] farr;//原生数组 Object[] oarr;//引用数组 int[][] arr2;//二维数组
class IntArray { private native int sumArray(int[] arr);//本地方法 public static void main(String[] args) { IntArray p = new IntArray(); int arr[] = new int[10]; for (int i = 0; i < 10; i++) { arr[i] = i; } int sum = p.sumArray(arr);//调用本地方法 System.out.println("sum = " + sum); } static { System.loadLibrary("IntArray"); } }
/* This program is illegal! */ JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) { int i, sum = 0; for (i = 0; i < 10; i++) { sum += arr[i]; } }
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) { jint buf[10]; jint i, sum = 0; (*env)->GetIntArrayRegion(env, arr, 0, 10, buf);//拷贝到一个jint buffer里进行操作 for (i = 0; i < 10; i++) { sum += buf[i]; } return sum; }
下面介绍如何访问原生数据类型数组的方法:
上个例子,我们用GetIntArrayRegion 将数据拷贝到一个C buffer里, 10是数组的长度,因为我们知道数组的长度是10,所以不会越界。JNI支持SetIntArrayRegion函数,来操作改变Int型数组中的某些数据,其他类型 float boolean 也支持,SetFloatArrayRegion.
JNI支持一族函数Get/Release<Type>ArrayElements, 重写上面的例子:
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) { jint *carr; jint i, sum = 0; carr = (*env)->GetIntArrayElements(env, arr, NULL); if (carr == NULL) { return 0; /* exception occurred */ } for (i=0; i<10; i++) { sum += carr[i]; } (*env)->ReleaseIntArrayElements(env, arr, carr, 0); return sum; }
JNI操作数组的函数:
我们如何选择这些函数呢?
如果你需要拷贝数组到一个预分配的buffer Get/Set<Type>ArrayRegion是最好的选择,越界会抛ArrayIndexOutOfBoundsException,适合小数组,因为这涉及到在栈上分配数组,小数组代价小,并且这个函数可以拷贝子数组,subarray
如果数组大小未知,可以尝试Get/ReleasePrimitiveArrayCritical 前提是Java 2 SDK release 1.2., 并且要多加小心。
使用Get/Release<type>ArrayElements 总是安全的,并且1.1, 1.2 版本通用,要么直接返回一个指针给你,要么拷贝一份数据,然后将拷贝的数据的指针给你。
访问引用类型数组:另一对JNI函数 GetObjectArrayElement 根据给定的index返回数据 SetObjectArrayElement 同理,和原生数据类型的数组不同,你一次拿不到那么多的数据,Get/Set<Type>ArrayRegion 是不支持的。
字符串和数组都是引用类型,你可以用Get/SetObjectArrayElement 来操作字符串数组和数组的数组(二维数组).
如下代码访问一个二维数组,然后打印数组中的内容:
class ObjectArrayTest { private static native int[][] initInt2DArray(int size);//初始化二维数组 public static void main(String[] args) { int[][] i2arr = initInt2DArray(3);//3x3大小 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { System.out.print(" " + i2arr[i][j]); } System.out.println(); } } static { System.loadLibrary("ObjectArrayTest"); } }
JNIEXPORT jobjectArray JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size)//注意一下,返回值是jobjectArray,你可能会觉得是jintArray { jobjectArray result; int i; jclass intArrCls = (*env)->FindClass(env, "[I");//[I代表int[] if (intArrCls == NULL) { return NULL; /* exception thrown */ } <pre name="code" class="cpp">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;}
0 1 2
1 2 3
2 3 4
DeleteLocalRef调用保证了不会因为申请内存而发生out-of-memory ,后面详细介绍为什么要调用这个方法。
上一篇:JNI官方文档翻译2-Getting Started
标签:
原文地址:http://blog.csdn.net/mtaxot/article/details/51423204