Android jni/ndk编程二:jni数据类型转换(primitive,String,array)

一.数据类型映射概述

从咱们开始jni编程起,就不可能避开函数的参数与返回值的问题。java语言的数据类型和c/c++有不少不一样的地方,因此咱们必须考虑当在java层调用c/c++函数时,怎么正确的把java的参数传给c/c++函数,怎么正确的从c/c++函数获取正确的函数返回值;反之,当咱们在c/c++中使用java的方法或属性时,如何确保数据类型能正确的在java和c/c++之间转换。 
回顾咱们上一篇文章中的那个c函数:java

#include <stdio.h> #include <jni.h> #include <stdlib.h> JNIEXPORT jstring JNICALL Java_com_jinwei_jnitesthello_MainActivity_sayHello(JNIEnv * env, jobject obj){ return (*env)->NewStringUTF(env,"jni say hello to you"); }

这个函数很是简单,它没有接受参数,可是它返回了一个字符串给java层。咱们不能简单的使用return “jni say hello to you”;而是使用了NewStringUTF函数作了个转换,这就是数据类型的映射。 
普通的jni函数通常都会有两个参数:JNIEnv * env, jobject obj,第三个参数起才是该函数要接受的参数,因此这里说它没有接受参数。android

1.1JNIEnv * env

JNIEnv是一个线程相关的结构体, 该结构体表明了 Java 在本线程的运行环境 。这意味不一样的线程各自拥有各一个JNIEnv结构体,且彼此之间互相独立,互不干扰。NIEnv结构包括了JNI函数表,这个函数表中存放了大量的函数指针,每个函数指针又指向了具体的函数实现,好比,例子中的NewStringUTF函数就是这个函数表中的一员。 
JVM,JNIEnv与native function的关系可用以下图来表述: 
这里写图片描述c++

1.2 jobject obj

这个参数的意义取决于该方法是静态仍是实例方法(static or an instance method)。 
当本地方法做为一个实例方法时,第二个参数至关于对象自己,即this. 当本地方法做为 
一个静态方法时,指向所在类. 在本例中,sayHello方法是实例方法,因此obj就至关于this指针。编程

二.基本数据类型的映射

在Java中有两类数据类型:primitive types,如,int, float, char;另外一种为 
reference types,如,类,实例,数组。 
java基本类型与c/c++基本类型能够直接对应,对应方式由jni规范定义: 
这里写图片描述 
JNI基本数据类型的定义在jni.h中:数组

typedef unsigned char jboolean; /* unsigned 8 bits */ typedef signed char jbyte; /* signed 8 bits */ typedef unsigned short jchar; /* unsigned 16 bits */ typedef short jshort; /* signed 16 bits */ typedef int jint; /* signed 32 bits */ typedef long long jlong; /* signed 64 bits */ typedef float jfloat; /* 32-bit IEEE 754 */ typedef double jdouble; /* 64-bit IEEE 754 */

也就是说jni.h中定义的数据类型已是c/c++数据类型了,咱们使用jint等类型的时候要明白其实使用的就是int 数据类型。ruby

2.1实践

咱们在上一篇博客中实现的程序的基础上进一步实验,如今给native_sayHello函数添加一个参数,c代码以下:app

#include <stdio.h> #include <jni.h> #include <stdlib.h> jstring native_sayHello(JNIEnv * env, jobject obj,jint num){ char array[30]; snprintf(array,30,"jni accept num : %d",num); return (*env)->NewStringUTF(env,array); } static JNINativeMethod gMethods[] = { {"sayHello", "(I)Ljava/lang/String;", (void *)native_sayHello}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv* env = NULL; //注册时在JNIEnv中实现的,因此必须首先获取它 jint result = -1; if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4) != JNI_OK) //从JavaVM获取JNIEnv,通常使用1.4的版本 return -1; jclass clazz; static const char* const kClassName="com/jinwei/jnitesthello/MainActivity"; clazz = (*env)->FindClass(env, kClassName); //这里能够找到要注册的类,前提是这个类已经加载到java虚拟机中。 这里说明,动态库和有native方法的类之间,没有任何对应关系。 if(clazz == NULL) { printf("cannot get class:%s\n", kClassName); return -1; } if((*env)->RegisterNatives(env,clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK) //这里就是关键了,把本地函数和一个java类方法关联起来。无论以前是否关联过,一概把以前的替换掉! { printf("register native method failed!\n"); return -1; } return JNI_VERSION_1_4; }

这里仍是使用动态的方式注册本地方法,相比较以前的代码,这里只作了两处修改: 
1.native_sayHello增长了一个参数jint num; 
2.函数签名也随之改变: “(I)Ljava/lang/String;”,以前是 “()Ljava/lang/String;” 
Android层的代码也随之改变:ide

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); String hehe = this.sayHello(12345); textView.setText(hehe); } public native String sayHello(int num);

这样就会在TextView中显示出jni accept num : 12345。 
这个实验验证了java基本类型能够直接对应到c/c++的基本类型。函数

三.字符串的转换

java的String与c/c++的字符串有很大不一样,两者之间不能直接对应,其转换须要经过jni函数来实现。 
jni支持Unicode和utf-8两种编码格式的转换。Unicode表明的了16-bit字符集,utf-8则兼容ASCII码,java虚拟机使用的Unicode编码,c/c++则默认使用ASCII码。这由于jni支持Unicode和utfbain吗之间的转换,因此咱们可使用Jni规范提供的函数在java与c/c++之间转换数据类型。 
这一节咱们从字符串提及,jni使用的字符串类型是jstring,咱们先看看它的定义: 
c++中:工具

class _jobject {}; class _jstring : public _jobject {}; typedef _jstring* jstring;

可见在c++中jstring是_jsting*类型的指针,_jstring是一个继承了_jobject类的类。 
c中:

typedef void* jobject; typedef jobject jstring;

可见jstring就是一个void *类型的指针。

3.1 java->native

java虚拟机传递下来的字符串是存储在java虚拟机内部的字符串,这个字符串固然使用的是Unicode编码了,使用c编程的时候,传递下来的jstring类型是一个void *类型的指针,它指向java虚拟机内部的一个字符串,咱们不能使用这个字符串,是由于它的编码方式是Unicode编码,咱们须要把它转换为utf-8编码格式,这样咱们就能够在c/c++中访问这个转换后的字符串了。 
咱们可使用jni规范提供的一下连个函数转换Unicode编码和utf-8编码:

const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*); void (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);

使用GetStringUTFChars函数时,要记得检测其返回值,由于调用该函数会有内存分配操做,失败后,该函数返回NULL,并抛OutOfMemoryError异常。 
调用完GetStringUTFChars函数后,咱们还要调用ReleaseStringUTFChars函数释放在GetStringUTFChars中分配的内存。不释放的话就会出现内存泄漏了。

3.2 native->java

有了以上两个函数,咱们就能够把java中的字符串转换为c/c++中使用的字符串了,而把c/c++使用的字符串转换为java使用的字符串这件事咱们以前已经作过了,咱们可使用使用NewStringUTF构造java.lang.String;若是此时没有足够的内存,NewStringUTF将抛OutOfMemoryError异常,同时返回NULL。 
NewStringUTF定义以下:

jstring     (*NewStringUTF)(JNIEnv*, const char*);

可见它接受一个char 类型的指针,char 就是咱们能够在c/c++中用来指向字符串的指针了。 
经过以上的学习,咱们能够尝试使用这三个方法作个验证了,仍是在原来的基础上修改.

3.3实站

此次实战,咱们要开始使用android的logcat工具了,这个工具能够打印一些Log出来,方便咱们使用。使用android logcat只需三步: 
1.包含头文件

#include <android/log.h>

2.配置Android.mk 
在Android.mk中添加: LOCAL_LDLIBS := -llog 
3.定义LOGI,LOGE等宏 
在使用前:

#define LOG_TAG "HELLO_JNI" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

这样咱们就可使用LOGI,LOGE来打印咱们的Log了。 
jni方法只是改成传递一个字符串进去:

jstring native_sayHello(JNIEnv * env, jobject obj,jstring str){
    LOGE("JNI: native_sayHello"); char array[50]; char array1[50]; const char * local_str = (*env)->GetStringUTFChars(env,str,NULL); LOGE("local_str: %s,length:%d",local_str,strlen(local_str)); strncpy(array,local_str,strlen(local_str)+1); (*env)->ReleaseStringUTFChars(env,str,local_str); LOGE("array: %s",array); snprintf(array1,sizeof(array),"jni : %s",array); LOGE("array1: %s",array1); return (*env)->NewStringUTF(env,array1); }

咱们只是作了轻微的改动,native_sayHello接受一个jstring类型的参数,咱们把这个参数转换为utf-8格式的字符串,而后添加一点咱们Local的信息,而后再返回给java层。假如咱们在android中这样调用native方法: 
String hehe = this.sayHello(“Java say hello to jni”); 
此时,app就会显示: 
jni:java say hello to jni

以上咱们对java字符串和c/c++之间字符串的转换作了简单的学习和尝试。jni规范还提供了许多用于字符串处理的函数:

jsize       (*GetStringLength)(JNIEnv*, jstring); const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*); void (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);

这三个函数用于操做unicode字符串。当jstring指向一个unicode字符串时,咱们可使用GetStringLength获取这个unicode字符串的长度。unicode字符串不一样于utf-8格式的字符串,utf-8格式的字符串一‘\0’结尾,unicode编码的字符串则不一样,所以,咱们须要GetStringLength来得到unicode字符串的长度。 
GetStringChars与ReleaseStringChars用来获取和释放unicode编码的字符串,通常不怎么用,但若是操做系统支持unicode编码的话,这两个函数会颇有用。 
GetStringChars与GetStringUTFChars的第三个参数须要作进一步解释。若是咱们使用他们中的某一个函数从JVM中得到一个字符串,咱们不知道这个字符串是指向JVM中的原始字符串仍是是一份原始字符串的拷贝,但咱们能够经过第三个参数来得知这一信息。这一信息是有用的,咱们知道JVM中的字符串是不能更改的,若是得到的字符串是JVM中的原始字符串,第三个参数就为JNI_FALSE,那咱们不能够修改它,但若是它是一份拷贝,第三个参数就为JNI_TRUE,则意味着咱们能够修改它。 
一般咱们不关心它,只须要把第三个参数置为NULL便可。

四.jdk 1.2中新的字符串操做函数

4.1Get/RleaseStringCritical

为尽量的避免内存分配,返回指向java.lang.String内容的指针,Java 2 SDKrelease 1.2提供了:Get/RleaseStringCritical. 这对函数有严格的使用原则。当使用这对函数时,这对函数间的代码应被当作临界区(critical region). 在该代码区,不要调用任何会阻塞当前线程和分配对象的JNI函数,如IO之类的操做。上述原则,能够避免JavaVM执行GC。由于在执行Get/ReleaseStringCritical区的代码 
时,GC被禁用了,若是因某些缘由在其余线程中引起了JavaVM执行GC操做,VM有死锁的危险:当前线程A进入Get/RelaseStringCritical区,禁用了GC,若是其余线程B中有GC请求,因A线程禁用了GC,因此B线程被阻塞了;而此时,若是B线程被阻塞时已经得到了一个A线程执行后续工做时须要的锁;死锁发生了。 
jni不支持Get/RleaseStringUTFCritical这样的函数,由于一旦涉及到字符串编码的转换,便使java虚拟机产生对数据的赋值行为,这样没法避免没存分配。 
为避免死锁,此时应尽可能避免调用其余JNI方法。

4.2GetStringRegion/GetStringUTFRegion

这两个函数的做用是向准备好的缓冲区写数据进去。

void        (*GetStringRegion)(JNIEnv*, jstring, jsize, jsize, jchar*); void (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);

简单用法举例:

char outbuf[128]; int len = (*env)->GetStringLength(env, prompt); (*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf);

其中prompt是java层传下来的字符串。这个函数能够直接把jstring类型的字符串写入到outbuf中。这个函数有三个参数:第一个是outbuf的其实位置,第二个是写入数据的长度,第三个参数是outbuf。注意,数据的长度是unicode编码的字符串长度,咱们须要使用GetStringLength来获取。

注:GetStringLength/GetStringUTFLength这两个函数,前者是Unicode编码长度,后者 
是UTF编码长度。

五.jni字符串操做函数总结

且看下图: 
这里写图片描述

六.jni字符串操做函数总结

对于小尺寸字串的操做,首选Get/SetStringRegion和Get/SetStringUTFRegion,由于栈 
上空间分配,开销要小的多;并且没有内存分配,就不会有out-of-memory exception。如 
果你要操做一个字串的子集,这两个函数函数的starting index和length正合要求。

GetStringCritical/ReleaseStringCritical函数的使用必须很是当心,他们可能致使死锁。

七.访问数组

JNI处理基本类型数组和对象数组的方式是不一样的。

7.1访问基本类型数组

JNI支持经过Get/ReleaseArrayElemetns返回Java数组的一个拷贝(实现优良的 
VM,会返回指向Java数组的一个直接的指针,并标记该内存区域,不容许被GC)。 
jni.h中的定义以下:

jboolean*   (*GetBooleanArrayElements)(JNIEnv*, jbooleanArray, jboolean*); jbyte* (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*); jchar* (*GetCharArrayElements)(JNIEnv*, jcharArray, jboolean*); jshort* (*GetShortArrayElements)(JNIEnv*, jshortArray, jboolean*); jint* (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*); jlong* (*GetLongArrayElements)(JNIEnv*, jlongArray, jboolean*); jfloat* (*GetFloatArrayElements)(JNIEnv*, jfloatArray, jboolean*); jdouble* (*GetDoubleArrayElements)(JNIEnv*, jdoubleArray, jboolean*); void (*ReleaseBooleanArrayElements)(JNIEnv*, jbooleanArray, jboolean*, jint); void (*ReleaseByteArrayElements)(JNIEnv*, jbyteArray, jbyte*, jint); void (*ReleaseCharArrayElements)(JNIEnv*, jcharArray, jchar*, jint); void (*ReleaseShortArrayElements)(JNIEnv*, jshortArray, jshort*, jint); void (*ReleaseIntArrayElements)(JNIEnv*, jintArray, jint*, jint); void (*ReleaseLongArrayElements)(JNIEnv*, jlongArray, jlong*, jint); void (*ReleaseFloatArrayElements)(JNIEnv*, jfloatArray, jfloat*, jint); void (*ReleaseDoubleArrayElements)(JNIEnv*, jdoubleArray, jdouble*, jint);

GetArrayRegion函数能够把得到的数组写入一个提早分配好的缓冲区中。 
SetArrayRegion能够操做这一缓冲区。 
其定义以下:

void (*GetBooleanArrayRegion)(JNIEnv*, jbooleanArray, jsize, jsize, jboolean*); void (*GetByteArrayRegion)(JNIEnv*, jbyteArray, jsize, jsize, jbyte*); void (*GetCharArrayRegion)(JNIEnv*, jcharArray, jsize, jsize, jchar*); void (*GetShortArrayRegion)(JNIEnv*, jshortArray, jsize, jsize, jshort*); void (*GetIntArrayRegion)(JNIEnv*, jintArray, jsize, jsize, jint*); void (*GetLongArrayRegion)(JNIEnv*, jlongArray, jsize, jsize, jlong*); void (*GetFloatArrayRegion)(JNIEnv*, jfloatArray, jsize, jsize, jfloat*); void (*GetDoubleArrayRegion)(JNIEnv*, jdoubleArray, jsize, jsize, jdouble*); /* spec shows these without const; some jni.h do, some don't */ void (*SetBooleanArrayRegion)(JNIEnv*, jbooleanArray, jsize, jsize, const jboolean*); void (*SetByteArrayRegion)(JNIEnv*, jbyteArray, jsize, jsize, const jbyte*); void (*SetCharArrayRegion)(JNIEnv*, jcharArray, jsize, jsize, const jchar*); void (*SetShortArrayRegion)(JNIEnv*, jshortArray, jsize, jsize, const jshort*); void (*SetIntArrayRegion)(JNIEnv*, jintArray, jsize, jsize, const jint*); void (*SetLongArrayRegion)(JNIEnv*, jlongArray, jsize, jsize, const jlong*); void (*SetFloatArrayRegion)(JNIEnv*, jfloatArray, jsize, jsize, const jfloat*); void (*SetDoubleArrayRegion)(JNIEnv*, jdoubleArray, jsize, jsize, const jdouble*);

GetArrayLength函数能够得到数组中元素个数,其定义以下:

jsize       (*GetArrayLength)(JNIEnv*, jarray);

jni操做原始类型数组的函数总结以下: 
这里写图片描述

7.2实战尝试

咱们修改以前的native_sayHello函数,让它接受数组做为参数:

jint native_sayHello(JNIEnv * env, jobject obj,jintArray arr){
    jint *carr;
    jint i, sum = 0; carr = (*env)->GetIntArrayElements(env, arr, NULL); if (carr == NULL) { return 0; /* exception occurred */ } jint length = (*env)->GetArrayLength(env,arr); for (i=0; i<length; i++) { LOGE("carr[%d]=%d",i,carr[i]); sum += carr[i]; } (*env)->ReleaseIntArrayElements(env, arr, carr, 0); return sum; }

不要忘记修改函数签名:

static JNINativeMethod gMethods[] = { {"sayHello", "([I)I", (void *)native_sayHello}, }; 

java层调用以下:

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); int a[] = {1,2,3,4,5,6,7,8,9}; String hehe = "num: "+String.valueOf(this.sayHello(a)); textView.setText(hehe); } public native int sayHello(int [] arr);

打印以下:

09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[0]=1 09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[1]=2 09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[2]=3 09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[3]=4 09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[4]=5 09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[5]=6 09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[6]=7 09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[7]=8 09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[8]=9

7.3访问对象数组

对于对象数组的访问,使用Get/SetObjectArrayElement,对象数组只提供针对数组的每 
个元素的Get/Set,不提供相似Region的区域性操做。 
对象数组的操做主要有以下三个函数:

jobjectArray (*NewObjectArray)(JNIEnv*, jsize, jclass, jobject);
    jobject (*GetObjectArrayElement)(JNIEnv*, jobjectArray, jsize); void (*SetObjectArrayElement)(JNIEnv*, jobjectArray, jsize, jobject);

这三个方法的使用经过下面的实战来说解。

7.4实战体验

咱们在下面的实战中做以下尝试: 
1.java层传入一个String a[] = {“hello1”,”hello2”,”hello3”,”hello4”,”hello5”}; 
2.native打印出每个值。 
3.native建立一个String数组, 
4.返回给java,java显示出这个字符串数组的全部成员 
native实现方法: 
咱们新增一个函数来实现上面要求。

jobjectArray native_arrayTry(JNIEnv * env, jobject obj,jobjectArray arr){
    jint length = (*env)->GetArrayLength(env,arr); const char * tem ; jstring larr; jint i=0; for(i=0;i<length;i++){ //1.得到数组中一个对象 larr = (*env)->GetObjectArrayElement(env,arr,i); //2.转化为utf-8字符串 tem = (*env)->GetStringUTFChars(env,larr,NULL); //3.打印这个字符串 LOGE("arr[%d]=%s",i,tem); (*env)->ReleaseStringUTFChars(env,larr,tem); } jobjectArray result; jint size = 5; char buf[20]; //1.获取java.lang.String Class jclass intArrCls = (*env)->FindClass(env,"java/lang/String"); if (intArrCls == NULL) { return NULL; /* exception thrown */ } //2. 建立java.lang.String数组 result = (*env)->NewObjectArray(env, size, intArrCls, NULL); if (result == NULL) { return NULL; /* out of memory error thrown */ } //3.设置数组中的每一项,分别为jni0,jni1,jni2,jni3,jni4 for (i = 0; i < size; i++) { larr = (*env)->GetObjectArrayElement(env,result,i); snprintf(buf,sizeof(buf),"jni%d",i); (*env)->SetObjectArrayElement(env, result, i,(*env)->NewStringUTF(env,buf) ); } //4.返回array return result; }

方法签名:

static JNINativeMethod gMethods[] = { {"sayHello", "([I)I", (void *)native_sayHello}, {"arrayTry","([Ljava/lang/String;)[Ljava/lang/String;",(void *)native_arrayTry}, }; 

java层调用:

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); String a[] = {"hello1","hello2","hello3","hello4","hello5"}; String []strings = this.arrayTry(a); StringBuilder stringBuilder = new StringBuilder(); for(String s:strings){ stringBuilder.append(s); } textView.setText(stringBuilder.toString()); } public native int sayHello(int []arr); public native String[] arrayTry(String [] arr); }
相关文章
相关标签/搜索