Android JNI开发(5)--数据传递

来源:互联网 发布:基金规模 知乎 编辑:程序博客网 时间:2024/06/05 19:01

首先JNI中的数据类型有基本数据类型和引用类型:
基本数据类型为:

Java类型        JNI类型         描述boolean        jboolean      unsigned 8 bitsbyte           jbyte         signed 8 bitschar           jchar         unsigned 16 bitsshort          jshort        signed 16 bitsint            jint          signed 32 bitslong           jlong         signed 64 bitsfloat          jfloat        32 bitsdouble         jdouble       64 bitsvoid           void          void

引用类型为:

jobject     jclass (java.lang.Class objects)    jstring (java.lang.String objects)    jarray (arrays)         jobjectArray (object arrays)        jbooleanArray (boolean arrays)        jbyteArray (byte arrays)        jcharArray (char arrays)        jshortArray (short arrays)        jintArray (int arrays)        jlongArray (long arrays)        jfloatArray (float arrays)        jdoubleArray (double arrays)    jthrowable (java.lang.Throwable objects)

明白了这些,就可以在Java和JNI代码对接的时候,处理好形参的类型。

1、字符串传递

(1)从Java传递到native代码
首先,Java代码:

public native String sumStr(String strA,String strB);  

然后,native方法如下:

JNIEXPORT jstring JNICALL Java_com_example_mystore_MainActivity_sumStr(          JNIEnv * pJNIEnv, jobject pThis, jstring a, jstring b) {      //将Java层的字符串转成char*类型的    const char* aString = (*pJNIEnv)->GetStringUTFChars(pJNIEnv, a, 0);      const char* bString = (*pJNIEnv)->GetStringUTFChars(pJNIEnv, b, 0);      //之后就可以进行相应的处理    char* buf[1024];      strcpy(buf, aString);      strcat(buf, bString);      (*pJNIEnv)->ReleaseStringUTFChars(pJNIEnv, a, aString);      (*pJNIEnv)->ReleaseStringUTFChars(pJNIEnv, b, bString);      return (*pJNIEnv)->NewStringUTF(pJNIEnv, buf);  }  

由于java程序中传过去的String对象在本地方法中对应的是jstring类型,jstring类型和c中的char*不同,所以如果你直接当做 char*使用的话,就会出错。因此在使用之前需要将jstring转换成为c/c++中的char*,这里使用JNIEnv的方法转换。
这里其实只是得到a的一个copy.所以用完后,还需要通过 ReleaseStringUTFChars这方法进行释放。GetStringUTFChars 和 ReleaseStringUTFChars 这两方法需要成对出现.
(2)从native传递字符串到java:

//处理字符串追加  JNIEXPORT jstring JNICALL Java_cn_itcast_ndk3_DataProvider_sayHelloInC    (JNIEnv * env, jobject obj, jstring str){      char* newstr = "native string";       return (*env)->NewStringUTF(env, newstr);  }  

NewStringUTF返回值为jstring。这里是直接return ,如果需要调用Java方法,把jstring值作为参数传入,也可以实现。这里就不实现了。

2、传递基本类型数据

很多情况下,比如图片数据处理,我们都需要将图像数据以数据的形式,传递到native代码中进行相应的处理,然后在传递回java层。
(1)Java层传递到native层

//处理Java传过来的char[] jcharArray test_chararray (JNIEnv * env, jclass, jcharArray j_char_array){ //首先计算数组长度     jsize j_char_array_size = env->GetArrayLength(j_char_array);      //获取数据第一个数据的地址    jchar* j_c = env->GetCharArrayElements(j_char_array, 0);      //打印      for (int i = 0; i < j_char_array_size; i++){          __android_log_print(ANDROID_LOG_INFO, COM_THINKING_J_DATA_TOOLS_LOG_TITLE, "array index %i is %i from %x", i, *(j_c + i), (j_c + i));          if (*(j_c + i) >= 97 && *(j_c + i) <= 122){              *(j_c + i) -= 32;          }      }     //接下来是在本地接收    //先创建一个相同大小的数组     jcharArray result_array = env->NewCharArray(j_char_array_size);    //获取新创建的的数组的第一个数据的地址  ,其实也就是相当于获取到了全部的数据    //第一个是数组,第二个是数组里面开始的元素    jchar * result = env->GetCharArrayElements(result_array, 0);      if (j_char_array_size > 0){      //复制到新的数据中,第一个参数是目的数组,第二个是源数组,第三个是size        memcpy(result, j_c, j_char_array_size*sizeof(jchar));      }      //释放    env->ReleaseCharArrayElements(j_char_array, j_c, 0);      env->ReleaseCharArrayElements(result_array, result, 0);      return result_array;  }  //另一个处理方法,处理Java过来的int[]jint IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) {     jint buf[10];     jint i, sum = 0;     //从0开始复制长度为10的数据从arr到buf中     (*env)->GetIntArrayRegion(env, arr, 0, 10, buf);     for (i = 0; i < 10; i++) {         sum += buf[i];     }     return sum; }//处理Java传过来的byte[]jboolean  processByteArray(JNIEnv *env, jobject object,jbyteArray passwd,jint len){   jbyte *bytes;   unsigned char *buf;   int i;    //从jbytearray获取数据到jbyte*   bytes = env->GetByteArrayElements(passwd,NULL);   if(bytes == NULL) {      return false;   }   buf =(unsigned char *)calloc(len,sizeof(char));   if(buf == NULL)   {      return false;   }   for(i=0;i<len;i++)   {      *(buf+i)=(unsigned char)(*(bytes+i));   }    //释放资源   env->ReleaseByteArrayElements(passwd,bytes,0);   __android_log_write(ANDROID_LOG_ERROR,"TAG",(char*)buf);   free(buf);   return true;}

我们可以看到,native中有两种方法可以接收Java传过来的数组:一个 GetByteArrayRegion,另一个是 GetByteArrayElements ,前者是进行值拷贝,将Java端数组的数据拷贝到本地的数组中,后者是指针的形式,将本地的数组指针直接指向Java端的数组地址,其实本质上是JVM在堆上分配的这个数组对象上增加一个引用计数,保证垃圾回收的时候不要释放,从而交给本地的指针使用,使用完毕后指针一定要记得通过ReleaseByteArrayElements进行释放,否则会产生内存泄露。
(2)native返回数组值Java层
把Jni层定义的数组传递到Java层,一般有两种方法,一种是通过native函数的返回值来传递,另一种是通过jni层回调java层的函数来传递,后者多用于jni的线程中。无论哪种方法,都离不开 SetByteArrayRegion 函数,该函数将本地的数组数据拷贝到了 Java 端的数组中。

//传递jintArray 到javaJNIEXPORT jintArray JNICALL getIntArray(JNIEnv * pJNIEnv, jobject pThis) {      //  先创建      jintArray lJavaArray = (*pJNIEnv)->NewIntArray(pJNIEnv, 4);      if (lJavaArray == NULL) {          return NULL;      }      int target[] = { 2, 4, 6, 8 };      //从target复制从0开始长度为4的数组,到lJavaArray中    (*pJNIEnv)->SetIntArrayRegion(pJNIEnv, lJavaArray, 0, 4, target);      return lJavaArray;  }  //向Java层返回Byte数组JNIEXPORT jintArray JNICALL setByteArray(JNIEnv * pJNIEnv, jobject pThis) {      unsigned char buffer[10];    jbytearray array = env->NewByteArray(10);//创建一个新的bytearray,长度为10    for(int i = 0;i < 10; i++){        buffer[i] = i;    }    //从buffer复制从0开始长度为4的数组,到array中    env->SetByteArrayRegion(array,0,10,buffer);}

3、传递对象数组

废话不说,直接上代码:
(1)从Java端传到native:

JNIEXPORT void JNICALL saveDogArr(          JNIEnv * pJNIEnv, jobject pThis, jobjectArray joa) {      //先得到个数      jsize lLength = (*pJNIEnv)->GetArrayLength(pJNIEnv, joa);      //分配内存      jobject* lArray = (jobjectArray*) malloc(lLength * sizeof(jobject));      //将传入的数据放入      int i = 0;      for (i = 0; i < lLength; ++i) {          jobject obj = (*pJNIEnv)->GetObjectArrayElement(pJNIEnv, joa, i);          lArray[i] = (*pJNIEnv)->NewGlobalRef(pJNIEnv, obj);          (*pJNIEnv)->DeleteLocalRef(pJNIEnv, obj);      }      globalObjectArray = lArray;      gLength = lLength;  }  

(2)从native传到Java:

JNIEXPORT jobjectArray JNICALL getDogArr(          JNIEnv * pJNIEnv, jobject pThis) {      // 先找到class      jclass dogClass = (*pJNIEnv)->FindClass(pJNIEnv, "com/xxxx/Dog");      //检查是否找到dogclass      if (dogClass == NULL) {          CCLog("AAAA", "没有找到dogClass");          return NULL;      }    //创建一个对象数组    jobjectArray joa = (*pJNIEnv)->NewObjectArray(pJNIEnv, gLength, dogClass,              NULL);      (*pJNIEnv)->DeleteLocalRef(pJNIEnv, dogClass);      int i = 0;      for (i = 0; i < gLength; ++i) {      //第一个参数为jnievn,第二个是目标数组,第三个是在在目标数组中的index,第四个是将填进去的对象        (*pJNIEnv)->SetObjectArrayElement(pJNIEnv, joa, i,                  globalObjectArray[i]);          if ((*pJNIEnv)->ExceptionCheck(pJNIEnv)) {              CCLog("AAAA", "[SetObjectArrayElement] 里面出现错误!!!");              return NULL;          }      }      return joa;  }  

4、JNI和Java共享内存空间

首先,对上面的几种类型做一些总结:
(1)Java传到JNI,使用GetByteArrayRegion的方式:
该方法的本质是将Java端数组数据拷贝到本地的数组中,所以在JNI对数据修改后Java端的数据并没有改变。
使用GetPrimitiveArrayCritical。
GetPrimitiveArrayCritical 表面上可以得到底层数据指针,在JNI层修改数组时Java层的数据也会变。But,如果只使用GetPrimitiveArrayCritical获取数据,程序运行一段时间内存会crash。所以,使用GetPrimitiveArrayCritical时必须使用ReleasePrimitiveArrayCritical ,通过测试发现当数据量大时执行ReleasePrimitiveArrayCritical会非常耗时。
(2)JNI传到Java:
把Jni层的数组传递到Java层,一般有两种方法,一种是通过native函数的返回值来传递,另一种是通过jni层回调java层的函数来传递,后者多用于jni的线程中或是数据量较大的情况。无论哪种方法,都离不开 SetByteArrayRegion 函数,该函数将本地的数组数据拷贝到了 Java 端的数组中。
请注意,上面的方式都涉及到内存复制,根据实战经验,在Android系统中,一旦数据量变大,拷贝一次内存将非常耗时。所以上述方式在追求效率时不推荐使用。解决的方法可以尝试让JAVA层和JNI共享内存的方式。最后找到了两种方式。

使用共享内存的方式
(1)使用GetByteArrayElements方式
该方式是指针的形式,将本地的数组指针直接指向Java端的数组地址,其实本质上是JVM在堆上分配的这个数组对象上增加一个引用计数,保证垃圾回收的时候不要释放,从而交给本地的指针使用,使用完毕后指针一定要记得通过ReleaseByteArrayElements进行释放,否则会产生内存泄露。

unsigned char* psrcImg = (unsigned char*)(env->GetByteArrayElements(srcImg,0));  unsigned char* pBufferI420 = (unsigned char*) (env->GetByteArrayElements(dstImg,0));if (psrcImg == NULL || pBufferI420 == NULL){  return -1;}env->ReleaseByteArrayElements(srcImg,(jbyte*)psrcImg,0);env->ReleaseByteArrayElements(dstImg,(jbyte*)pBufferI420,0);

(2)使用Direct Buffer 方式传递。
Java和Jni层的数组传递还有一个比较重要的方式,就是通过Direct Buffer来传递,这种方式类似于在堆上创建创建了一个Java和Jni层共享的整块内存区域,无论是Java层或者Jni层均可访问这块内存,并且Java端与Jni端同步变化,由于是采用的是共享内存的方式,因此相比于普通的数组传递,效率更高,但是由于构造/析构/维护这块共享内存的代价比较大,所以小数据量的数组建议还是采用上述方式,Direct Buffer方式更适合长期使用频繁访问的大块内存的共享。具体可使用GetDirectBufferAddress获得共享的内存地址。
比如在涉及到RTSP获取预览图像数据的时候,需要一帧一帧的发送到Java,就可以使用这种方法:

JNIEnv *env ;if (savedVm->AttachCurrentThread(&env, 0) != 0){    LOGI("Failed to attach current thread");    return;}//第一个参数是数据,类型为void*,第二个为数据大小jobject buf = env->NewDirectByteBuffer(frame_mjpeg->data, frame_mjpeg->actual_bytes);//调起Java的方法,作为参数传入env->CallVoidMethod(mObject, on_new_frame_method_id,buf);env->ExceptionClear();env->DeleteLocalRef(buf);savedVm->DetachCurrentThread();

在JNI层,尽量避免使用内存拷贝。

原创粉丝点击