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 * 转string
jstring 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