HAL/JNI简明笔记(四)——字符串及数组

来源:互联网 发布:什么是大数据分析 编辑:程序博客网 时间:2024/05/16 10:54

字符串操作

基本数据类型在JNI中和Java一样,可以直接访问java传过来的参数,但是关于数组和字符串就不能直接使用了。因为java所有类传到c来都是指针,但是在JVM中结构对外不可见,要使用JNIEnv指向的函数表中选择合适的函数指针来调用。例如JNI代码片如下:

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello    (JNIEnv *env, jclass cls, jstring j_str)  {      const char *c_str = NULL;      char buff[128] = {0};      jboolean isCopy;    // 返回JNI_TRUE表示原字符串的拷贝,返回JNI_FALSE表示返回原字符串的指针      c_str = (*env)->GetStringUTFChars(env, j_str, &isCopy);      printf("isCopy:%d\n",isCopy);      if(c_str == NULL)      {          return NULL;      }      printf("C_str: %s \n", c_str);      sprintf(buff, "hello %s", c_str);      (*env)->ReleaseStringUTFChars(env, j_str, c_str);      return (*env)->NewStringUTF(env,buff);  }  
访问java.lang.String对应的JNI类型jstring时,没有像访问基本数据类型一样直接使用,因为它在Java是一个引用类型,所以在本地代码中只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容。函数接收一个jstring类型的参数j_str,但jstring类型是指向JVM内部的一个字符串,和C风格的字符串类型char*不同,所以在JNI中不能通把jstring当作普通C字符串一样来使用,必须使用合适的JNI函数来访问JVM内部的字符串数据结构。

GetStringUTFChars(env, j_str, &isCopy) 

参数一,JNIEnv函数表指针

参数二,Java传递给本地代码的字符串指针

参数三,一般不关心,直接填NULL

返回值,为jni能使用的c风格的字符串指针

java默认unicode编码,c默认UTF-8编码。jni使用申请内存要检查,失败后GetStringUTFChars会返回NULL,并抛出一个OutOfMemoryError异常。JNI的异常和Java中的异常处理流程是不一样的,Java遇到异常如果没有捕获,程序会立即停止运行。而JNI遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非常危险的,因此,我们需要用return语句跳过后面的代码,并立即结束当前方法。既然GetStringUTFChars是在JVM中申请空间,那么使用完后就要告诉JVM可以释放了,这个函数就是ReleaseStringUTFChars,像这样的函数其实还有好多,基本格式就是GetXXX和ReleaseXXX成对使用。

对于NewStringUTF()这个函数就是JVM申请一个新的java.lang.String字符串对象并转换为java识别的unicode编码。

另在c和c++中使用env参数去调用JNIEnv函数列表有不一样的地方,我在编译时碰到过这样的错误,比如c代码使用(*env)->NewStringUTF(env,buff);而c++中使用env->NewStringUTF(env,buff);这个区别在于jni.h中的定义

#if defined(__cplusplus)typedef _JNIEnv JNIEnv;typedef _JavaVM JavaVM;#elsetypedef const struct JNINativeInterface* JNIEnv;typedef const struct JNIInvokeInterface* JavaVM;#endif
JNIEnv在c++中是结构体,c中是结构体指针。

其他字符串操作

1、const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*)和void (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*),这对函数和Get/ReleaseStringUTFChars函数功能差不多,用于获取和释放以Unicode格式编码的字符串,后者是用于获取和释放UTF-8编码的字符串。

2、 jsize (*GetStringLength)(JNIEnv*, jstring),由于UTF-8编码的字符串以'\0'结尾,而Unicode字符串不是。如果想获取一个指向Unicode编码的jstring字符串长度,在JNI中可通过这个函数获取。

3、 jsize (*GetStringUTFLength)(JNIEnv*, jstring),获取UTF-8编码字符串的长度,也可以通过标准C函数strlen获取

4、 GetStringCritical和ReleaseStringCritical:提高JVM返回源字符串直接指针的可能性,返回char *

Get/ReleaseStringChars和Get/ReleaseStringUTFChars这对函数返回的源字符串会后分配内存,如果有一个字符串内容相当大,有1M左右,而且只需要读取里面的内容打印出来,用这两对函数就有些不太合适了,因为他们要复制。此时用Get/ReleaseStringCritical可直接返回源字符串的指针应该是一个比较合适的方式。不过这对函数有一个很大的限制,在这两个函数之间的本地代码不能调用任何会让线程阻塞或等待JVM中其它线程的本地函数或JNI函数。因为通过GetStringCritical得到的是一个指向JVM内部字符串的直接指针,获取这个直接指针后会导致暂停GC(Gabage Collection)线程,当GC被暂停后,如果其它线程触发GC继续运行的话,都会导致阻塞调用者。所以在Get/ReleaseStringCritical这对函数中间的任何本地代码都不可以执行导致阻塞的调用或为新对象在JVM中分配内存,否则,JVM有可能死锁。另外一定要记住检查是否因为内存溢出而导致它的返回值为NULL,因为JVM在执行GetStringCritical这个函数时,仍有发生数据复制的可能性,尤其是当JVM内部存储的数组不连续时,为了返回一个指向连续内存空间的指针,JVM必须复制所有数据。

下面代码(xx.c)演示这对函数的正确用法:

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello    (JNIEnv *env, jclass cls, jstring j_str)  {      const jchar* c_str= NULL;      char buff[128] = "hello ";      char* pBuff = buff + 6;      /*      * 在GetStringCritical/RealeaseStringCritical之间是一个关键区。      * 在这关键区之中,绝对不能呼叫JNI的其他函数和会造成当前线程中断或是会让当前线程等待的任何本地代码,      * 否则将造成关键区代码执行区间垃圾回收器停止运作,任何触发垃圾回收器的线程也会暂停。      * 其他触发垃圾回收器的线程不能前进直到当前线程结束而激活垃圾回收器。      */      c_str = (*env)->GetStringCritical(env,j_str,NULL);   // 返回源字符串指针的可能性      if (c_str == NULL)  // 验证是否因为字符串拷贝内存溢出而返回NULL      {          return NULL;      }      while(*c_str)       {          *pBuff++ = *c_str++;      }      (*env)->ReleaseStringCritical(env,j_str,c_str);      return (*env)->NewStringUTF(env,buff);  }
JNI中没有Get/ReleaseStringUTFCritical这样的函数,因为在进行编码转换时很可能会促使JVM对数据进行复制,因为JVM内部表示的字符串是使用Unicode编码的。
5、 void (*GetStringRegion)(JNIEnv*, jstring, jsize, jsize, jchar*)和void (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*),分别表示获取Unicode和UTF-8编码字符串指定范围内的内容。这对函数会把源字符串复制到一个预先分配的缓冲区内。用例如下(xx.c)

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello    (JNIEnv *env, jclass cls, jstring j_str)  {      jsize len = (*env)->GetStringLength(env,j_str);  // 获取unicode字符串的长度      printf("str_len:%d\n",len);      char buff[128] = "hello ";      char* pBuff = buff + 6;      // 将JVM中的字符串以utf-8编码拷入C缓冲区,该函数内部不会分配内存空间      (*env)->GetStringUTFRegion(env,j_str,0,len,pBuff);      return (*env)->NewStringUTF(env,buff);  }
参数一,是JNI函数表

参数二,是java下的String

参数三,从第几个开始拷贝

参数四,拷贝几个字符

参数五,拷贝到JNI中的buff[ ]

上例中就是将java的j_str中的从第0个开始拷贝len个字符到pBuff指向的空间中。

GetStringUTFRegion这个函数会做越界检查,如果检查发现越界了,会抛出StringIndexOutOfBoundsException异常,这个方法与GetStringUTFChars比较相似,不同的是,GetStringUTFRegion内部不分配内存,不会抛出内存溢出异常。

注意:GetStringUTFRegion和GetStringRegion这两个函数由于内部没有分配内存,是程序员先申请好buffer,所以JNI没有提供ReleaseStringUTFRegion和ReleaseStringRegion这样的函数。


字符串操作总结:


1、对于小字符串来说,GetStringRegion和GetStringUTFRegion这两对函数是最佳选择,因为缓冲区可以被编译器提前分配,而且永远不会产生内存溢出的异常。当你需要处理一个字符串的一部分时,使用这对函数也是不错。因为它们提供了一个开始索引和子字符串的长度值。另外,复制少量字符串的消耗 也是非常小的。

2、使用GetStringCritical和ReleaseStringCritical这对函数时,必须非常小心。一定要确保在持有一个由 GetStringCritical 获取到的指针时,本地代码不会在 JVM 内部分配新对象,或者做任何其它可能导致系统死锁的阻塞性调用

3、获取Unicode字符串和长度,使用GetStringChars和GetStringLength函数

4、获取UTF-8字符串的长度,使用GetStringUTFLength函数

5、创建Unicode字符串,使用NewStringUTF函数

6、从Java字符串转换成C/C++字符串,使用GetStringUTFChars函数

7、通过GetStringUTFChars、GetStringChars、GetStringCritical获取字符串,这些函数内部会分配内存,必须调用相对应的ReleaseXXXX函数释放内存


数组操作

JNI中的数组分为基本类型数组和对象数组,它们的处理方式是不一样的,基本类型数组中的所有元素都是JNI的基本数据类型,可以直接访问。而对象数组中的所有元素是一个类的实例或其它数组的引用,和字符串操作一样,不能直接访问Java传递给JNI层的数组,必须选择合适的JNI函数来访问和设置Java层的数组对象。


1、基本类型数组

package com.study.jnilearn;    // 访问基本类型数组  public 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 < arr.length; i++) {              arr[i] = i;          }          int sum = p.sumArray(arr);          System.out.println("sum = " + sum);      }        static {          System.loadLibrary("IntArray");      }  } 
JNI(xx.c)代码:

JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray  (JNIEnv *env, jobject obj, jintArray j_array)  {      jint i, sum = 0;      jint *c_array;      jint arr_len;      //1. 获取数组长度      arr_len = (*env)->GetArrayLength(env,j_array);      //2. 根据数组长度和数组元素的数据类型申请存放java数组元素的缓冲区      c_array = (jint*)malloc(sizeof(jint) * arr_len);      //3. 初始化缓冲区      memset(c_array,0,sizeof(jint)*arr_len);      printf("arr_len = %d ", arr_len);      //4. 拷贝Java数组中的所有元素到缓冲区中      (*env)->GetIntArrayRegion(env,j_array,0,arr_len,c_array);      for (i = 0; i < arr_len; i++) {          sum += c_array[i];  //5. 累加数组元素的和      }      free(c_array);  //6. 释放存储数组元素的缓冲区      return sum;  }
例中,在Java中定义了一个sumArray的native方法,参数类型是int[],对应JNI中jintArray类型。在本地代码中,首先通过JNI的GetArrayLength函数获取数组的长度,已知数组是jintArray类型,可以得出数组的元素类型是jint,然后根据数组的长度和数组元素类型,申请相应大小的缓冲区。如果缓冲区不大的话,当然也可以直接在栈上申请内存,那样效率更高,但是没那么灵活,因为Java数组的大小变了,本地代码也跟着修改。接着调用GetIntArrayRegion函数将Java数组中的所有元素拷贝到C缓冲区中,并累加数组中所有元素的和,最后释放存储java数组元素的C缓冲区,并返回计算结果。

jsize (*GetArrayLength)(JNIEnv*, jarray)能获取jarray子类类型的数组长度,void (*GetIntArrayRegion)(JNIEnv*, jintArray, jsize, jsize, jint*)它的使用方法和字符串中的GetStringRegion类似。基本类型的数组名字模式如Get<Type>ArrayRegion,其中<Type>有Boolean,Byte,Char,Short,Int,Long,Float,Double。另JNI还提供了一个和GetIntArrayRegion相对应的函void (*SetIntArrayRegion)(JNIEnv*, jintArray, jsize, jsize, const jint*),本地代码可以通过这个函数来修改所有基本数据类型数组的元素,同时也有8种Set<Type>ArrayRegion函数。

另外JNI还提供一系列直接获取数组元素指针的函数Get/Release<Type>ArrayElements,也有8种,个人认为跟GetIntArrayRegion差不多,以此种类型改写上例(xx.c)

JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray2  (JNIEnv *env, jobject obj, jintArray j_array)  {      jint i, sum = 0;      jint *c_array;      jint arr_len;      // 可能数组中的元素在内存中是不连续的,JVM可能会复制所有原始数据到缓冲区,然后返回这个缓冲区的指针      c_array = (*env)->GetIntArrayElements(env,j_array,NULL);      if (c_array == NULL) {          return 0;   // JVM复制原始数据到缓冲区失败      }      arr_len = (*env)->GetArrayLength(env,j_array);      printf("arr_len = %d\n", arr_len);      for (i = 0; i < arr_len; i++) {          sum += c_array[i];      }      (*env)->ReleaseIntArrayElements(env,j_array, c_array, 0); // 释放可能复制的缓冲区      return sum;  }
GetIntArrayElements第三个参数表示返回的数组指针是原始数组,还是拷贝原始数据到临时缓冲区的指针,如果是JNI_TRUE:表示临时缓冲区数组指针,JNI_FALSE:表示临时原始数组指针。开发当中,我们并不关心它从哪里返回的数组指针,这个参数填NULL即可,但在获取到的指针必须做校验,因为当原始数据在内存当中不是连续存放的情况下,JVM会复制所有原始数据到一个临时缓冲区,并返回这个临时缓冲区的指针。有可能在申请开辟临时缓冲区内存空间时,会内存不足导致申请失败,这时会返回NULL。


有时会被GC阻塞,为此JNI提供了Get/ReleasePrimitiveArrayCritical这对函数,针对8种基本类型可用。本地代码在访问数组对象时会暂停GC线程。不过使用这对函数也有个限制,在Get/ReleasePrimitiveArrayCritical这两个函数期间不能调用任何会让线程阻塞或等待JVM中其它线程的本地函数或JNI函数,和处理字符串的Get/ReleaseStringCritical函数限制一样。这对函数和GetIntArrayElements函数一样,返回的是数组元素的指针。以此种类型改写上例中(xx.c)

JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray  (JNIEnv *env, jobject obj, jintArray j_array)  {      jint i, sum = 0;      jint *c_array;      jint arr_len;      jboolean isCopy;      c_array = (*env)->GetPrimitiveArrayCritical(env,j_array,&isCopy);      printf("isCopy: %d \n", isCopy);      if (c_array == NULL) {          return 0;      }      arr_len = (*env)->GetArrayLength(env,j_array);      printf("arr_len = %d\n", arr_len);      for (i = 0; i < arr_len; i++) {          sum += c_array[i];      }      (*env)->ReleasePrimitiveArrayCritical(env, j_array, c_array, 0);      return sum;  }


小结

1、对于小量的、固定大小的数组,应该选择Get/SetArrayRegion函数来操作数组元素是效率最高的。因为这对函数要求提前分配一个C临时缓冲区来存储数组元素,你可以直接在Stack(栈)上或用malloc在堆上来动态申请,当然在栈上申请是最快的。有人可能会认为,访问数组元素还需要将原始数据全部拷贝一份到临时缓冲区才能访问而觉得效率低?我想告诉你的是,像这种复制少量数组元素的代价是很小的,几乎可以忽略。这对函数的另外一个优点就是,允许你传入一个开始索引和长度来实现对子数组元素的访问和操作(对应的SetArrayRegion函数可以修改数组),不过传入的索引和长度不要越界,函数会进行检查,如果越界了会抛出ArrayIndexOutOfBoundsException异常。

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

3、Get/Release<type>ArrayElements系列函数永远是安全的,JVM会选择性的返回一个指针,这个指针可能指向原始数据,也可能指向原始数据的复制。

4、JNI如果创建IntArray等8种基本数组并返回给java用,如用jintArray (*NewIntArray)(JNIEnv*, jsize);。


2、引用类型数组

JNI提供了两个函数来访问对象数组,GetObjectArrayElement返回数组中指定位置的元素,SetObjectArrayElement修改数组中指定位置的元素。与基本类型不同的是,我们不能一次得到数据中的所有对象元素或者一次复制多个对象元素到缓冲区。因为字符串和数组都是引用类型,只能通过Get/SetObjectArrayElement这样的JNI函数来访问字符串数组或者数组中的数组元素。下例从jni中返回二维int数组,

package com.study.jnilearn;    public class ObjectArray {        private native int[][] initInt2DArray(int size);            public static void main(String[] args) {          ObjectArray obj = new ObjectArray();          int[][] arr = obj.initInt2DArray(3);          for (int i = 0; i < 3; i++) {              for (int j = 0; j < 3; j++) {                  System.out.format("arr[%d][%d] = %d\n", i, j, arr[i][j]);              }          }      }        static {          System.loadLibrary("ObjectArray");      }  
对应的jni代码片(xx.c),
JNIEXPORT jobjectArray JNICALL Java_com_study_jnilearn_ObjectArray_initInt2DArray    (JNIEnv *env, jobject obj, jint size)  {      jobjectArray result;      jclass clsIntArray;      jint i,j;      // 1.获得一个int型二维数组类的引用      clsIntArray = (*env)->FindClass(env,"[I");      if (clsIntArray == NULL)      {          return NULL;      }      // 2.创建一个数组对象(里面每个元素用clsIntArray表示)      result = (*env)->NewObjectArray(env,size,clsIntArray,NULL);      if (result == NULL)      {          return NULL;      }            // 3.为数组元素赋值      for (i = 0; i < size; ++i)      {          jint buff[256];          jintArray intArr = (*env)->NewIntArray(env,size);          if (intArr == NULL)          {              return NULL;          }          for (j = 0; j < size; j++)          {              buff[j] = i + j;          }          (*env)->SetIntArrayRegion(env,intArr, 0,size,buff); //拷贝填充一维数组int[];基本数组类型操作        (*env)->SetObjectArrayElement(env,result, i, intArr);  //将一维int[]作为元素,组成二维int[][];引用数组类型操作        (*env)->DeleteLocalRef(env,intArr); //利用env申请的局部空间但不返回给java的删除     }            return result;  }

本地函数initInt2DArray首先调用JNI函数FindClass获得一个int型的二维数组类的引用,传递给FindClass的参数"[I"是JNI class descript(JNI类型描述符),它对应着JVM中的int[]类型。如果int[]类加载失败的话,FindClass会返回NULL,然后抛出一个java.lang.NoClassDefFoundError: [I异常。

     接下来,NewObjectArray创建一个新的数组,这个数组里面的元素类型用intArrCls(int[])类型来表示。函数NewObjectArray只能分配第一维,JVM没有与多维数组相对应的数据结构,JNI也没有提供类似的函数来创建二维数组。由于JNI中的二维数组直接操作的是JVM中的数据结构,相比JAVA和C/C++创建二维数组要复杂很多。给二维数组设置数据的方式也非常直接,首先用NewIntArray创建一个JNI的int数组,并为每个数组元素分配空间,然后用SetIntArrayRegion把buff[]缓冲中的内容复制到新分配的一维数组中去,最后在外层循环中依次将int[]数组赋值到jobjectArray数组中,一维数组中套一维数组,就形成了一个所谓的二维数组。

     另外,为了避免在循环内创建大量的JNI局部引用,造成JNI引用表溢出,所以在外层循环中每次都要调用DeleteLocalRef将新创建的jintArray引用从引用表中移除。在JNI中,只有jobject以及子类属于引用变量,会占用引用表的空间,jint,jfloat,jboolean等都是基本类型变量,不会占用引用表空间,即不需要释放。引用表最大空间为512个,如果超出这个范围,JVM就会挂掉。




JNI函数实际上还有很多,详见jni.h。他们的用法及说明见oracle官网(http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html)或者译文PDF(http://download.csdn.net/detail/u011430511/7425867).


经过自己理解后,整合进来,大部分与下面博客类似,大部分系转,谢谢他们的文章,其大部分也是引用“JNI官方规范中文版”的译文。

ref: xyang0917 JNI/NDK开发指南
a345017062 JNI官方规范中文版

0 0