JNI(2)--JNI函数使用(I)

61 阅读13分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

JNI系列文章导引

JNI(1)---JNI入门介绍

JNI(2)--JNI函数使用(I)

JNI(3)--JNI函数使用(II)

JNI(4)--JNI中的api说明

JNI函数使用

本文介绍在JNI中函数的使用,以及和Java互调用。

字符串操作

在JNI中,jstring指向JVM中一个字符串类型的数据,但是和常规的C字符串char*不同,需要将jstring转成char*在C代码中使用,以及将char*转成jstring返回给Java中。

  • jstringchar*

    • 分配内存的方式

      GetStringUTFChars:将jstring转成一个UTF-8的C字符串

      ReleaseStringUTFChars:在字符串使用完毕或告诉JVM回收

      GetStringChars:将jstring转成Unicode格式的C字符串

      ReleaseStringChars:使用Unicode格式的字符串资源

      GetStringLength:获取UTF-8以及Unicode编码的字符串长度,如果是UTF-8的,也可以使用strlen获取到

      以上get方法,JVM会为新的字符串分配内存,如果内存太少而失败,会返回NULL,并抛出OutOfMemoryErrory异常

      以上get方法的签名中,最后一个参数是jboolean *isCopy。这个参数的意思是如果得到的字符串是原始字符串的拷贝,就会被赋值成JNI_TRUE,如果是同一份数据,就会赋值成JNI_FALSE。这个取决与JVM,通常不需要关心,只需要传NULL即可

      在字符串使用完毕后需要调用Releasexxx进行回收

    • 小字符串不需要分配内存

      GetStringRegion/GetStringUTFRegion:将字符串存放到缓存区中。不会分配内存,但是会做越界检查

    • 其他转换方法

      GetStringCritical/ReleaseStringCritical。这个功能和GetStringChars/ReleaseStringChars类似,如果可能的话虚拟机会返回一个指向字符串元素的指针;否则,则返回一个复制的副本。

      注意:

      这两个函数的使用使用有很大的限制。在使用这两个函数时,这两个函数中间的代码不能调用任何让线程阻塞或者等待JVM的其他线程的本地函数或者JNI函数。有了这些限制,JVM就可以在本地方法持有一个从GetStringCritical得到的字符串的指指针时禁止GC。当GC被禁止时,任何线程如果出发GC的话,都会被阻塞。而Get/ReleaseStringChars这两个函数中间的任何本地代码都不可以执行会导致阻塞的调用或者为新对象在JVM中分配内存。

      总之,为了避免死锁,在GetStringCriticalReleaseStringCritical之间不要调用任何JNI函数。

  • char*jstring

    NewStringUTF:在本地方法中创建一个String字符串对象,创建失败会返回NULL,并抛出OutOfMemoryErrory异常

 JNIEXPORT jstring JNICALL
 Java_com_example_jnifirst_MyString_getLine(JNIEnv *env, jobject thiz, jstring text) {
     //将jni字符串转成c字符串,最后一个参数传NULL,让JVM指定是赋值还是直接返回原始数据,但是不管如何,都必须调用release
     const char *str = (*env)->GetStringUTFChars(env, text, NULL);
     if (str == NULL) {
         //为新的字符串分配内存,若因内存不足会分配失败会返回NULL,并抛出OutOfMemory错误,但是不会改变程序的执行流
         return NULL;
     }
     LOGD("%s", str);
     //使用完毕后需要释字符串占用的内存
     (*env)->ReleaseStringUTFChars(env, text, str);
     //返回新创建的一个字符串
     char *hello = "hello world from getLine";
     //将c字符串转成jni字符串返回
     return (*env)->NewStringUTF(env, hello);
 }

数组操作

数组的引用类型一般是jarray,或者其子类型jintArray等。jarray也需要转成C数组才能使用,所以不要直接访问jarray,必须使用合适的JNI函数访问对应类型的数组。

基本数组

  • 转成C数组

    Get\Release<Type>ArrayElements:获取一个指向基本类型数组的元素的指针,会分配内存,不足会返回NULL,并抛出OutOfMemoryErrory异常

    Get<Type>ArrayRegion:将基本类型的数据拷贝到一个预先分配的C缓冲区中,不会分配内存,但是会做越界检查

    Set<Type>ArrayRegion:修改基本类型的数组中的元素

    Get/ReleasePrimitiveArrayCritical:就像GetStringCritical使用一样要小心线程死锁

    GetArrayLength:返回数组中的元素个数

     JNIEXPORT jint JNICALL
     Java_com_example_jnifirst_MyIntArray_sumArray2(JNIEnv *env, jobject thiz, jintArray array) {
         //获取数组的长度
         jint length = (*env)->GetArrayLength(env, array);
     ​
         //将原始数组进行复制后,返回指向复制后的数组的指针
         jint *carr = (*env)->GetIntArrayElements(env, array, NULL);
         //分配内存,可能会失败,并抛出异常
         if (carr == NULL) {
             return 0;
         }
     ​
         jint sum = 0;
         for (int i = 0; i < length; ++i) {
             sum += carr[i];
         }
     ​
         //释放内存
         (*env)->ReleaseIntArrayElements(env, array, carr, 0);
         return sum;
     }
    
     JNIEXPORT jint JNICALL
     Java_com_example_jnifirst_MyIntArray_sumArray(JNIEnv *env, jobject thiz, jintArray array,
                                                   jint size) {
         //修改数组中的元素
         jint mo_buf[] = {10};
         //                                  开始的位置 修改的长度 源数组
         (*env)->SetIntArrayRegion(env, array, 0, 1, mo_buf);
     ​
         //将jni int数组转成c的数组
         jint buf[size];
         jint sum = 0;
         //将int数组中的所有元素赋值到一个C缓冲区中
         (*env)->GetIntArrayRegion(env, array, 0, size, buf);
     ​
         for (int i = 0; i < size; ++i) {
             sum += buf[i];
         }
         return sum;
     }
    
  • 创建基本类型数组

    JNI中有一系列的New<Type>Array的方法创建数组,创建后可以使用Set<Type>ArrayRegion进行数组各个item的赋值

     jint tmp[256];
     //1. 生成int数组
     jintArray iarr = (*env)->NewIntArray(env, size);
     if (iarr == NULL) {
     return NULL;
     }
     for (int j = 0; j < size; ++j) {
     tmp[j] = i + j;
     }
     //2.使用tmp给iarr数组item赋值
     (*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
    

对象数组

GetObjectArrayElement:返回数组中指定位置的元素

SetObjectArrayElement:修改数组中指定位置的元素

与基本类型数组不同,对象数组不能一次得到所有的对象元素或者一次复制多个对象元素

 JNIEXPORT jobjectArray JNICALL
 Java_com_example_jnifirst_MyObjectArray_init2DArray(JNIEnv *env, jobject thiz, jint size) {
     //findClass获取一个int数组引用
     jclass intArrCls = (*env)->FindClass(env, "[I");
     //类加载失败,会返回NULL,并抛出异常
     if (intArrCls == NULL) {
         return NULL;
     }
 ​
     //创建int二维数组对象,此时的对象是一个一维数组,二维数组就是数组的数组,那么一维数组的每个item都是一个数组
     jobjectArray result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
     //创建失败
     if (result == NULL) {
         return NULL;
     }
 ​
     for (int i = 0; i < size; ++i) {
         jint tmp[256];
         //为二维数组每个item都设置一个数组
 ​
         //1. 生成int数组
         jintArray iarr = (*env)->NewIntArray(env, size);
         if (iarr == NULL) {
             return NULL;
         }
         for (int j = 0; j < size; ++j) {
             tmp[j] = i + j;
         }
         //2.使用tmp给iarr数组item赋值
         (*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
         //3.将iarr设置给result
         (*env)->SetObjectArrayElement(env, result, i, iarr);
         //4.删除局部变量
         (*env)->DeleteLocalRef(env, iarr);
     }
     return result;
 }

访问Java字段

JNI提供函数可以获取和修改对象的字段以及静态字段

对象字段

 //kotlin
 private lateinit var s: String
     fun accessInstanceInJni() {
         s = "abc"
         accessField()
         Log.d(TAG, "in java:s = $s")
     }
     
 //jni
 ​
 JNIEXPORT void JNICALL
 Java_com_example_jnifirst_InstanceFieldAccess_accessField(JNIEnv *env, jobject thiz) {
     //访问java中的成员变量
 ​
     //1.获取类引用(thiz是对象引用)
     jclass jcls = (*env)->GetObjectClass(env, thiz);
     LOGD("in C:\n");
     //2. 获取成员字段id
     jfieldID fid = (*env)->GetFieldID(env, jcls, "s", "Ljava/lang/String;");
     if (fid == NULL) {
         //获取id失败
         return;
     }
     //3. 获取字段的值,对象字段使用的是jobject对象引用
     jobject jstr = (*env)->GetObjectField(env, thiz, fid);
     char *str = (*env)->GetStringUTFChars(env, jstr, NULL);
     if (str == NULL) {
         //out of memory
         return;
     }
     LOGD("c.s = %s\n", str);
     (*env)->ReleaseStringUTFChars(env, jstr, str);
     //4.修改字段的值
     jstring newStr = (*env)->NewStringUTF(env, "123");
     if (newStr == NULL) {
         //创建失败
         return;
     }
     //成员变量,要使用对象引用
     (*env)->SetObjectField(env, thiz, fid, newStr);
 }

Java中定义的字段s,在JNI方法中被赋值

访问一个字段的流程:

  1. 首先通过对象引用获取到类引用:jclass jcls = (*env)->GetObjectClass(env, thiz);
  2. 通过类引用调用GetFieldID获取field id、字段名字和字段描述符:fid = (*env)->GetFieldID(env, jcls, "s", "Ljava/lang/String;");
  3. 通过获取的field id来访问字段,在对象引用thiz上调用GetObjectField来访问字符串:jobject jstr = (*env)->GetObjectField(env, thiz, fid);

字符串和数组是特殊的对象,使用GetObjectField来访问对象字段。除了Get/SetObjectField外,还支持Get/Set<Type>Field来访问基本类型字段的函数。

静态字段

 //kotlin
 ​
 fun accessStaticFieldInJni() {
   accessStaticField()
     Log.d(TAG, "in java:si=$si")
 }
 private external fun accessStaticField();
 companion object {
   var si: Int = 0
 }
 ​
 //jni
 ​
 JNIEXPORT void JNICALL
 Java_com_example_jnifirst_StaticFieldAccess_accessStaticField(JNIEnv *env, jobject thiz) {
   //访问java中的静态成员变量
 ​
   //1.获取类引用
   jclass jcls = (*env)->GetObjectClass(env, thiz);
   LOGD("in C\n");
   //2.获取静态成员变量字段id
   jfieldID sfid = (*env)->GetStaticFieldID(env, jcls, "si", "I");
   if (sfid == NULL) {
     return;
   }
   //3.获取静态字段的值,静态变量,需要使用类jclass引用
   jint intSi = (*env)->GetStaticIntField(env, jcls, sfid);
   LOGD("c.si = %d", intSi);
   //4.修改静态字段的值
   (*env)->SetStaticIntField(env, jcls, sfid, 200);
 }

Java中定义了静态变量si,在JNI方法中进行访问和赋值

访问静态字段的流程和对象字段是一样的,区别在于:

  1. 在获取field id时,对象字段是使用GetFieldID,静态字段使用的是GetStaticFieldID
  2. 在获取和设置静态变量值时,对象字段使用Get/Set<Type>/ObjectField,静态字段使用的是Get/Set<Type>/ObjectField

访问Java方法

Java中实例方法必须在一个类的对象实例上调用。静态方法可以使用类进行调用

实例方法

 //kotlin
 ​
 fun callJavaMethodInJni(){
   nativeMethod()
 }
 private fun callByJni(){
   Log.d(TAG,"call in jni---")
 }
 private external fun nativeMethod()
 ​
 //jni
 ​
 JNIEXPORT void JNICALL
 Java_com_example_jnifirst_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject thiz) {
   //调用java中的方法
 ​
   //1.获取类引用
   jclass jcls = (*env)->GetObjectClass(env, thiz);
   LOGD("in C\n");
   //2.获取方法id
   jmethodID mid = (*env)->GetMethodID(env, jcls, "callByJni", "()V");
   if (mid == NULL) {
     return;
   }
   //3.调用方法,要使用对象引用,Void指方法的返回值
   (*env)->CallVoidMethod(env, thiz, mid);
 }

调用一个Java实例方法的流程:

  1. 通过GetObjectClass获取到类的对象引用:jclass jcls = (*env)->GetObjectClass(env, thiz);
  2. 通过GetMethodID获取到方法的id:jmethodID mid = (*env)->GetMethodID(env, jcls, "callByJni", "()V");如果获取不到会返回NULL,并会引发一个NoSuchMethodError错误
  3. 使用jobject引用thiz调用方法:(*env)->CallVoidMethod(env, thiz, mid);

JNI中还有支持其他返回值的方法的调用,如CallIntMethod等基本类型的,还有CallObjectMethod调用返回值是对象和数组的方法

如果需要调用其他类或者接口的方法,可以通过FindClass获取到类引用后,在获取方法id,然后调用方法

 //Thread对象
 jobject thd = ...;
 //获取Runnable类引用
 jclass runnableCls = (*env)->FindClass(env,"java/lang/Runnable");
 if(runnableCls == null){
   //handle error
   ...
 }
 //获取run方法的id
 jmethodID mid = (*env)->GetMethodID(env,runnableCls,"run","()V");
 if(mid == null){
   //handle error
   ...
 }
 (*env)->CallVoidMethod(env,thd,mid);

静态方法

JNI中调用Java中的静态方法,流程和调用实例方法类似,总体来看就是所有调用实例方法的函数都加上static;函数调用需要使用类引用

  1. 通过GetObjectClass获取到类的对象引用:jclass jcls = (*env)->GetObjectClass(env, thiz);
  2. 通过GeStatictMethodID获取到方法的id:jmethodID mid = (*env)->GetMethodID(env, jcls, "callByJni", "()V");如果获取不到会返回NULL,并会引发一个NoSuchMethodError错误
  3. 使用jclass类引用jcls调用方法:(*env)->CallStaticVoidMethod(env, jcls, mid);
 //kotlin
 ​
 fun callStaticMethodInJni() {
   staticNativeMethod()
 }
 private external fun staticNativeMethod()
   companion object {
   const val TAG = "StaticMethodCall"
     @JvmStatic
     fun callback(i: Int): String {
     Log.d(TAG, "call in jni i:$i")
       return "method in java"
   }
 }
 ​
 //jni
 ​
 JNIEXPORT void JNICALL
   Java_com_example_jnifirst_StaticMethodCall_staticNativeMethod(JNIEnv *env, jobject thiz) {
   //调用java中的静态方法
 ​
   //1. 获取类引用
   jclass jcls = (*env)->GetObjectClass(env, thiz);
   LOGD("in C:\n");
   //2. 获取静态方法id
   jmethodID smid = (*env)->GetStaticMethodID(env, jcls, "callback", "(I)Ljava/lang/String;");
   if (smid == NULL) {
     return;
   }
   //3. 调用静态方法,静态方法需要使用类引用;Object指返回值
   jint i = 100;
   jobject jReturnValue = (*env)->CallStaticObjectMethod(env, jcls, smid, i);
   const char *str = (*env)->GetStringUTFChars(env, jReturnValue, NULL);
   LOGD("str from java method return :%s", str);
 }

父类的实例方法

如果一个方法被定义在父类中,在子类中杯覆盖,也可以调用整个实例方法。JNI提供了一系列完成这些功能的函数:CallNovirtual<Type>/ObjectMethod。流程如下:

  1. 使用GetMethodId从一个指向父类的引用中获取到方法id
  2. 调用`CallNovirtual<Type>/ObjectMethod函数

这种情况很少遇到,因为在Java中很简单就能做到super.f()。但是CallNovirtual<Type>/ObjectMethod也可以用来调用父类的构造函数

构造函数

JNI中,构造函数可以和实例方法一样被调用,在调用GetMethodID时,方法名是<init>,V作为返回值类型;再通过NewObject传入方法id来调用构造函数,实例化一个对象。

下面例子实现JNI中NewString相同的功能:使用C字符串创建一个String对象jstring

 jstring MyNewString1(JNIEnv *env, jchar *chars, jint len) {
   //1. 找到一个String类
   jclass stringClass = (*env)->FindClass(env, "java/lang/String");
   if (stringClass == NULL) {
     return NULL;
   }
   //2. 找到String的构造函数id:String(byte[],String charset),指定编码,防止乱码(和MyNewString2对比即可看出),构造函数是<init>
   jmethodID cid = (*env)->GetMethodID(env, stringClass, "<init>", "([BLjava/lang/String;)V");
   if (cid == NULL) {
     return NULL;
   }
   //3. 创建char[]数组,并填充数组
   jbyteArray eleArr = (*env)->NewByteArray(env, len);
   if (eleArr == NULL) {
     return NULL;
   }
   (*env)->SetByteArrayRegion(env, eleArr, 0, len, (const jbyte *) chars);
   //4. 创建String对象,调用构造方法
   jstring charset = (*env)->NewStringUTF(env, "utf-8");
   jobject result = (*env)->NewObject(env, stringClass, cid, eleArr, charset);
   //5. 释放本地资源
   (*env)->DeleteLocalRef(env, stringClass);
   (*env)->DeleteLocalRef(env, eleArr);
   return result;
 }

既然可以使用JNI函数实现相同的功能,为什么JNI还需要提供NewString这样的内置函数呢?

原因是内置函数的效率远高于在本地代码里调用构造函数的API

还有一种方式,通过CallNonvirtualVoidMethod来调用构造函数,这种情况首先需要通过AllocObject创建一个未初始化的对象

 jstring MyNewString2(JNIEnv *env, jchar *chars, jint len) {
     //1. 找到一个String类
     jclass stringClass = (*env)->FindClass(env, "java/lang/String");
     if (stringClass == NULL) {
         return NULL;
     }
     //2. 找到String的构造函数id:String(char[]) ,构造函数是<init>
     jmethodID cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V");
     if (cid == NULL) {
         return NULL;
     }
     //3. 创建char[]数组,并填充数组
     jcharArray eleArr = (*env)->NewCharArray(env, len);
     if (eleArr == NULL) {
         return NULL;
     }
     (*env)->SetCharArrayRegion(env, eleArr, 0, len, chars);
     //4. 创建String对象,调用构造方法;使用CallNonvirtualVoidMethod
     //4.1首先使用AllocObject创建一个未初始化的对象
     jobject result = (*env)->AllocObject(env, stringClass);
     //4.2再使用使用CallNonvirtualVoidMethod调用构造函数
     if (result) {
         (*env)->CallNonvirtualVoidMethod(env, result, stringClass, cid, eleArr);
         //检查异常
         if ((*env)->ExceptionCheck(env)) {
             (*env)->DeleteLocalRef(env, result);
             return NULL;
         }
     }
     //5. 释放本地资源
     (*env)->DeleteLocalRef(env, stringClass);
     (*env)->DeleteLocalRef(env, eleArr);
     return result;
 }

AllocObject创建一个未初始化的对象,使用时一定要确保一个对象上面的构造函数最多被调用一次。如果需要首先创建一个未初始化对象,过一段时间再调用构造函数,这种方法就很有用。但是大部分情况下,还是应该使用NewObject,这个方法不容易出错。

缓存字段ID和方法ID

获取字段ID和方法ID时,需要用字段、方法的名字和描述符进行一个检索。检索过程相对比较耗时,可以缓存字段的ID和方法ID来减少这种消耗

使用时缓存

字段的ID和方法ID可以在字段的值被访问或者方法被回调的时候缓存起来。将ID保存到静态变量中,这样当再次使用时,不需要重新搜索ID

 JNIEXPORT void JNICALL
 Java_com_example_jnifirst_CacheFieldMethodId_cacheFieldIdNative(JNIEnv *env, jobject thiz) {
   static jfieldID sFieldId = NULL;
   //获取java中的成员变量,并缓存
 ​
   //1. 获取类
   jclass jcls = (*env)->GetObjectClass(env, thiz);
   LOGD("in C\n");
   //2. 获取成员变量id
   if (sFieldId == NULL) {
     sFieldId = (*env)->GetFieldID(env, jcls, "sField", "I");
     if (sFieldId == NULL) {
       return;
     }
   }
   //3.给获取成员赋值
   jint sFieldValue = (*env)->GetIntField(env, thiz, sFieldId);
   LOGD("sField value from java:%d", sFieldValue);
   (*env)->SetIntField(env, thiz, sFieldId, 100);
 }

获取字段ID,并缓存在static变量中,当static变量不为NULL时,就可以不需要再次搜索

初始化时缓存

在使用时缓存字段ID和方法ID的话,每次JNI方法中调用时都需要检查ID是否已经缓存。在许多情况下,在字段ID和方法ID被使用前就初始化是很方便的。时机在执行类的初始化时。

 private fun callback() {
   Log.d(TAG, "call in jni cached method id")
 }
 ​
 companion object {
   const val TAG = "CacheFieldMethodId"
 ​
   //类初始化时缓存id
   @JvmStatic
   private external fun initIds()
 ​
   init {
     //类初始化时缓存id
     initIds()
   }
 }

在JNI中缓存方法的ID到全局变量中

 //缓存的全局变量:callback方法的id
 jmethodID MID_CacheFieldMethodId_callback;
 ​
 JNIEXPORT void JNICALL
 Java_com_example_jnifirst_CacheFieldMethodId_initIds(JNIEnv *env, jclass clazz) {
     //将方法参数缓存在变量中
     MID_CacheFieldMethodId_callback = (*env)->GetMethodID(env, clazz, "callback", "()V");
 }

这样在每次使用时都不必判断是否缓存了。

两种方式对比

  1. 如果不能控制方法和字段所在的Java类源码的话,在使用时缓存是个理想的方案。

  2. 对比静态初始化缓存,使用时缓存存在一些缺点:

    1. 使用时缓存,每次使用时必须要检查是否缓存
    2. 方法 ID 和字段 ID 在类被 unload 时就会失效,如果在使用时缓存 ID,必须确保只要本地代码依赖于这个 ID 的值,那么这个类不被会 unload。另方面,如果缓存发生在静态初始化时,当类被 unload 和 reload 时,ID 会被重新计算。

    因此,尽可能在静态初始化时缓存字段 ID 和方法 ID。