首先JNI中的数据类型有基本数据类型和引用类型:
基本数据类型为:
Java |
native |
Size |
boolean |
jboolean |
unsigned 8 bits |
byte |
jbyte |
signed 8 bits |
char |
jchar |
unsigned 16 bits |
short |
jshort |
signed 16 bits |
int |
jint |
signed 32 bits |
lon g |
jlong |
signed 64 bits |
float |
jfloat |
32 bits |
double |
jdouble |
64 bits |
void |
void |
void |
1. 字符串传递
从Java传递到native代码
首先,Java代码:
public native String sumStr(String strA,String strB);
然后,native方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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的方法转换。
从native传递字符串到java
1 2 3 4 5 6
| //处理字符串追加 JNIEXPORT jstring JNICALL Java_cn_itcast_ndk3_DataProvider_sayHelloInC (JNIEnv * env, jobject obj, jstring str){ char* newstr = "native string"; return (*env)->NewStringUTF(env, newstr); }
|
2.传递基本类型数据
很多情况下,比如图片数据处理,我们都需要将图像数据以数据的形式,传递到native代码中进行相应的处理,然后在传递回java层。
Java层传递到native层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| //处理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进行释放,否则会产生内存泄露
native返回数组值Java层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| //传递jintArray 到java JNIEXPORT 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.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进行释放,否则会产生内存泄露。
1 2 3 4 5 6 7 8 9
| 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,就可以使用这种方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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层,尽量避免使用内存拷贝。