Android NDK开发之旅(3): 详解JNI数据类型与C/C++、Java之间的互调
Android NDK开发之旅(3):详解JNI数据类型与C/C++、Java之间的互调
(码字不易,转载请声明出处:http://blog.csdn.net/andrexpert/article/details/72851294)
1. JNI数据类型
JNI,Java NativeInterface,是一种为Java编写本地方法和JVM嵌入本地应用程序标准的应用程序接口,它允许运行在JVM上的Java代码能够与C/C++实现的本地库进行交互。
(1) JNI数据类型
Java中有两种类型:基本数据类型(int、float、char等)和引用类型(类、对象、数组等)。JNI定义了一个C/C++类型的集合,集合中每一个类型对应于Java中的每一个类型,其中,对于基本类型而言,JNI与Java之间的映射是一对一的,比如Java中的int类型直接对应于C/C++中的jint;而对引用类型的处理却是不同的,JNI把Java中的对象当作一个C指针传递到本地函数中,这个指针指向JVM中的内部数据结构,而内部数据结构在内存中的存储方式是不可见的,本地代码必须通过在JNIEnv中选择适当的JNI函数来操作JVM中的对象。比如,对于java.lang.String对应的JNI类型是jstring,但本地代码只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容。以下是JNI数据类型映射关系表,通过这种映射JNI就可以正确识别并转换Java数据类型:
映射 类型 | Java类型 | JNI本地类型 | C类型 | ||||
type/bits | type/bits/signatures | type/bits | |||||
基本 类型 | boolean | 8 | jboolean | u8 | Z | - |
|
byte | 8 | jbyte | 8 | B | - |
| |
char | 16 | jchar | u16 | C | char | 8 | |
short | 16 | jshort | 16 | S | short | 16 | |
int | 32 | jint | 32 | I | int | 32 | |
long | 64 | jlong | 64 | J | long | 32 | |
float | 32 | jfloat | 32 | F | float | 32 | |
double | 64 | jdouble | 64 | D | double | 64 | |
| void | - | Void | N/A | V | void | - |
引用 类型 | Object | - | jobject | - | - | - | - |
Class | - | jclass | - | L fully-qualified-class; | - | - | |
String | - | jstring | - | Ljava/lang/String; | - | - | |
arrays | - | jarray | - | - | - | - | |
object arrays | - | jobjectArray | - | [L fully-qualified-class; | - | - | |
boolean arrays | - | jbooleanArray | - | [Z | - | - | |
byte arrays | - | jbyteArray | - | [B | - | - | |
char arrays | - | jcharArray | - | [C | - | - | |
short arrays | - | jshortArray | - | [S | - | - | |
int arrays | - | jintArray | - | [I | - | - | |
long arrays | - | jlongArray | - | [J | - | - | |
float arrays | - | jfloatArray | - | [F | - | - | |
double arrays | - | jdoubleArray | - | [D | - | - | |
Throwable | - | jthrowable | - | Ljava/lang/Throwable; | - | - |
注:u8表示unsigned 8 bits;u16表示unsigned 16 bits;由于Java支持方法重载,在JNI访问Java层方法时仅靠函数名是无法唯一确定一个方法,因此JNI提供了一套签名规则(如:Z、B、[Z等),用一个字符串来唯一确定一个方法,其规则:(参数1类型签名参数2类型签名…)返回值类型签名,比如Java方法long getDeviceId(int n, String s, int[] arr)、long getDeviceId(int n)的类型签名分别为(ILjava/lang/String;[I)J、(I)J。
(2) 函数原型解析
JNIEXPORTjstring JNICALL Java_com_jiangdg_jnilearning_JNIUitls_ getDeviceId (JNIEnv*env, jobjectjobj, jstring j_str)或JNIEXPORTjstring JNICALL Java_com_jiangdg_jnilearning_JNIUitls_ getDeviceId (JNIEnv*env, jclass jcls,jstring j_str)
以上两个函数原型分别为两个Java本地方法到JNI层的映射,它映射的规则是:JNIEXPORT返回值类型JNICALL Java_包名_类名_方法名(JNIEnv*,jobject,参数1类型,…),其中JNIEXPORT和JNICALL为JNI的关键字,表示此函数是要被JNI调用的;JNIEnv接口指针指向一个函数表,函数表中的每一个入口指向一个JNI函数,因此通过该指针调用相关函数实现对Java引用的访问;jobject和jclass类型参数分别表明该Java本地方法是静态方法还是非静态方法,且jobject表示该非静态方法所属的对象,jclass表示该静态方法所属的类。
2. Java调用C/C++本地函数
(1) C or C++
/** * CPP源文件:Java访问本地函数,返回一个字符串 * */JNIEXPORTjstring JNICALL Java_com_jiangdg_jnilearning_JNIUitls_getStringFromC (JNIEnv *env, jclass jobj){ return env->NewStringUTF("My Nameis jiangdongguo--2017");} /** * C源文件:Java访问本地函数,返回一个字符串 * */JNIEXPORTjstring JNICALL Java_com_jiangdg_jnilearning_JNIUitls_getStringFromC (JNIEnv *env, jclass jobj){ return (*env)->NewStringUTF(env,"MyName is jiangdongguo--2017");}
上面两个函数原型,逻辑代码结构相似,其作用均为当Java层调用本地方法时向Java层返回一个UTF-8格式的字符串。然后,实际上它们的实现是截然不同的,比如一个需要传递env指针变量,另一个不需要,此外它们调用NewStringUTF的方式也不一样,究其原因,主要是因为这两个函数是在不同的源文件中实现的。通过查看jni.h源码可知,当源文件为.cpp时,JNIEnv实际为结构体JNIEnv_,然后我们再查看JNIEnv_结构体的内容,找到NewStringUTF(constchar *utf)函数,它实际执行了functions->NewStringUTF(this,utf)函数,而这个函数默认传递了一个this参数,该this参数则指的是getStringFromC函数原型中JNIEnv指针变量参数。因此,使用C++开发JNI时就无需再传递JNIEnv指针变量且在使用JNIEnv_结构体的成员时,直接使用结构体变量指向成员即可。
#ifdef__cplusplus// 如果为C++,JNIEnv表示JNIEnv_typedef JNIEnv_ JNIEnv;#else// 如果不为C++,JNIEnv表示JNINativeInterface_*typedef const struct JNINativeInterface_ * JNIEnv;#endif structJNIEnv_ { const struct JNINativeInterface_*functions;#ifdef__cplusplus……jstringNewStringUTF(const char *utf) { returnfunctions->NewStringUTF(this,utf);}……#endif
当源文件为.c时,JNIEnv实际表示的JNINativeInterface_*,JNIEnv*env即JNINativeInterface_**env,因此,我们在调用JNINativeInterface_结构体中的成员时需要使用一级指针来实现,即(*env)->成员。然后,再继续查看JNINativeInterface_源码,NewStringUTF函数需要传入一个JNIEnv结构体类型指针变量,该指针变量指向JNINativeInterface_结构体存储的地址,因此,还需要将变量env赋值给NewStringUTF即可。
structJNINativeInterface_ { …… jstring (JNICALL *NewStringUTF) (JNIEnv*env, const char *utf); ….}
(2) JNI中字符串处理
在JNI开发中,jstring类型指向JVM内部的一个字符串和C字符串类型char*是不同的,前者编码格式为Unicodec(UTF-16),后者为UTF-8。因此,我们不能简单的将jstring当作一个普通的C字符串来使用,必须要使用合适的JNI函数把jstring转化为C/C++字符串,即GetStringUTFChars。该函数可以把一个jstring指针(指向JVM内部的Unicode字符序列),转化为一个UTF-8格式的C字符串。需要注意的是,如果jstring字符串中包含中文,还需要将UTF-8转化为GB2312,否则会出现中文乱码。
JNIEXPORTjstring JNICALL Java_com_jiangdg_jnilearning_JNIUitls_getString2FromC (JNIEnv *env, jobject jobj, jstring j_str){ jsize len =env->GetStringLength(j_str); const char* c_str = (*env)->GetStringUTFChars(env,j_str,JNI_FALSE); // 内存不足,抛出OOM异常if(c_str == NULL){ return NULL;}// const char*转char*,拼接字符串char *c_tmp = (char *)malloc(len);stpcpy(c_tmp,c_str);strcat(c_tmp," 广州");// char * 转stringjstring j_temp = (*env)->NewStringUTF(env,(constchar*)c_tmp);// 释放本地字符串资源(*env)->ReleaseStringUTFChars(env,j_str,c_str);// 释放指针指向的内存资源free(c_tmp);return j_temp;}
注意:从GetStringUTFChars中获取的UTF-8字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars告诉JVM这个UTF-8字符串不会被使用了,因为这个UTF-8字符串占用的内存会被回收。
3. C/C++访问Java层方法、属性
JNI中提供了一系列函数可以用于访问对象的属性或者类的属性(静态属性),不仅可以获得该属性的值,还可以在本地代码中修改属性的值。总的来说,为了访问Java层的属性,本地方法需要做以下两步:
首先,如果是访问对象的属性,需要利用Java层传入本地的obj调用GetObjectClass获取该属性的类引用;
jclasscls = (*env)->GetObjectClass(env,jobj);
其次,通过在类引用上调用GetFieldID获取属性ID、属性名字和属性描述符,如果是静态字段,则是调用GetStaticFieldId方法。其中,key、count是属性的名称,"Ljava/lang/String;"或"I"是属性的类型;
jfieldID key_fid = (*env)->GetFieldID(env,cls,"key","Ljava/lang/String;");
jfieldID count_fid = (*env)->GetStaticFieldID(env, cls,"count","I");
第三,将对象obj和字段ID作为参数传入来访问属性,得到属性的值。其中,XXX为基本数据类型;
jstringkey = (jstring)(*env)->GetObjectField(env,jobj,对象属性ID);
jXXXarray arr = (jXXXarray)(*env)->GetXXXField(env,jobj,属性ID);;
jXXX count= (*env)->GetStaticXXXField(env,ju_cls,静态属性ID);
最后,如果我们需要修改属性的值,可以通过setXXXField或setStaticXXXField来修改;
(1) C/C++层访问对象的属性
/** * C/C++层访问Java对象的属性 * */JNIEXPORTjstring JNICALL Java_com_jiangdg_jnilearning_JNIUitls_accessJavaField (JNIEnv *env, jobject jobj){ // 得到Java类JNIUtils.class jclass jniutil_cls = (*env)->GetObjectClass(env,jobj); // 得到java对象的key属性ID jfieldID key_fid =(*env)->GetFieldID(env,jniutil_cls,"key","Ljava/lang/String;"); // 得到Java对象Key属性的值 jstring key =(jstring)(*env)->GetObjectField(env,jobj,key_fid); // 拼接一个新的c字符串 const char * c_key =(*env)->GetStringUTFChars(env,key,JNI_FALSE); char c_temp[100] = "Hello,"; strcat(c_temp,c_key); // 修改key属性的值 jstring j_temp =(*env)->NewStringUTF(env,c_temp); (*env)->SetObjectField(env,jobj,key_fid,j_temp); return j_temp;}
(2) C/C++层访问Java类的静态属性
/** * C/C++层访问Java类的静态属性 * */JNIEXPORT voidJNICALL Java_com_jiangdg_jnilearning_JNIUitls_accessJavaStaticField (JNIEnv *env, jobject jobj){ jclass ju_cls =(*env)->GetObjectClass(env,jobj); // 得到JNIUtils类静态属性count的ID jfieldID count_fid =(*env)->GetStaticFieldID(env,ju_cls,"count","I"); // 得到count属性的值 jint count =(*env)->GetStaticIntField(env,ju_cls,count_fid); // 修改count属性的值 jint new_count = count+1; (*env)->SetStaticIntField(env,ju_cls,count_fid,new_count);}
(3) C/C++层访问Java对象的方法
/** * C/C++层访问Java对象的方法 * */JNIEXPORT voidJNICALL Java_com_jiangdg_jnilearning_JNIUitls_accessJavaMethod (JNIEnv *env, jobject jobj){ jclass cls =(*env)->GetObjectClass(env,jobj); // 得到JNIUtils类对象genRandomInt方法的ID jmethodID mid =(*env)->GetMethodID(env,cls,"genRandomInt","(I)I"); // 调用genRandomInt方法 jint random =(*env)->CallIntMethod(env,jobj,mid,200); LOG_I("genRandomInt() =%d",random);}
(4) C/C++层访问Java类的静态方法
/** * C/C++层访问Java类的静态方法 * */JNIEXPORT voidJNICALL Java_com_jiangdg_jnilearning_JNIUitls_accessJavaStaticMethod (JNIEnv *env, jobject jobj){ jclass ju_cls =(*env)->GetObjectClass(env,jobj); // 得到JNIUtils类getUUID静态方法ID jmethodID mid =(*env)->GetStaticMethodID(env,ju_cls,"getUUID","()Ljava/lang/String;"); // 调用getUUID方法 jstring UUID =(jstring)(*env)->CallStaticObjectMethod(env,ju_cls,mid); LOG_I("getUUID() =%s",(*env)->GetStringUTFChars(env,UUID,JNI_FALSE));}
(5) C/C++层实现指向子类对象访问父类的方法
/** * C/C++访问Java的父类方法 * */JNIEXPORT voidJNICALL Java_com_jiangdg_jnilearning_JNIUitls_accessJavaFatherMethod (JNIEnv *env, jobject jobj){ jclass cls =(*env)->GetObjectClass(env,jobj); // 得到JNIUtils对象fruitInfo属性对象的ID jfieldID fid = (*env)->GetFieldID(env,cls,"fruitInfo","Lcom/jiangdg/jnilearning/Fruit;"); // 得到fruitInfo属性对象 jobject fruit_jobj =(*env)->GetObjectField(env,jobj,fid); // 得到父类Fruit jclass fruit_cls =(*env)->FindClass(env,"com/jiangdg/jnilearning/Fruit"); // 得到父类的printEatInfo方法,调用该方法 jmethodID fruit_mid =(*env)->GetMethodID(env,fruit_cls,"printEatInfo","()V"); (*env)->CallNonvirtualVoidMethod(env,fruit_jobj,fruit_cls,fruit_mid);}
4. 效果演示
GitHub项目地址:https://github.com/jiangdongguo/JniLearning
更多相关文章
- Android NDK环境创建方法简介
- Unity3D和Android之间的方法交互(jar模式)
- Android之LocationManager类的方法详解
- android init.rc 修改方法---adb shell 不能修改的原因
- android:configChanges属性在不同版本SDK下需要注意的事项