0%

Android JNI开发步骤三(数据传递)

首先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层,尽量避免使用内存拷贝。