JNI(Java Native Interface),它是java中的一套接口,用来跟c和c++通信。
JNI中的数据类型
java中的数据类型和c的数据类型之间的映射关系:
java->JNI->c/c++
基本数据类型:
java | JNI | |
---|---|---|
boolean | jboolean | |
byte | jbyte | |
char | jchar | |
short | jshort | |
int | jlong | |
long | jchar | |
float | jfloat | |
double | jdouble | |
void | void |
引用类型:
java | JNI | |
---|---|---|
String | jstring | |
object | jobject | |
class | jclass | |
byte[] | jByteArray | |
object[] | jobjectArray |
JNI开发流程的步骤
- 编写native方法
- javah命令,生成.h头文件
- 复制.h头文件到CPP工程中
- 复制jni.h和jni_md.h文件到CPP工程中
- 创建jni目录
- 添加本地支持add native support
- 实现.h头文件中声明的函数
- 生成动态库Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib
- 令执行Java程序,加载动态库
java调用C,例如下面返回一个字符串
1 | public class JniTest { |
C中1
2
3
4
5
6
7
8
9//函数实现
//Java_包名_类名_方法名
JNIEXPORT jstring JNICALL Java_com_chs_jni_JniTest_getStringFromC
(JNIEnv *env, jclass jcls){
//JNIEnv 结构体指针 env是二级指针
//代表Java运行环境,调用Java中的代码
//将C的字符串转为一个java字符串
return (*env)->NewStringUTF(env,"C String");
}
C调用java中的方法
访问属性,访问静态属性,访问方法,访问静态方法
先在java中定义相关的属性和方法
1 | public class JniTest { |
1.访问属性
这里会用到属性签名
数据类型 | 签名 | |
---|---|---|
boolean | Z | |
byte | B | |
char | C | |
short | S | |
int | I | |
long | L | |
float | F | |
double | D | |
void | V | |
object | L开头,然后以/分割包的完整类型,后面再加; 比如String的签名就是Ljava/long/String |
|
Array | 以[开头,在加上数组元素类型的签名, 比如int[],签名是[I,int[][]的签名是[[I, object[]的签名是[Ljava/lang/Object |
1 | JNIEXPORT jstring JNICALL Java_com_chs_JniTest_accessField |
2.访问静态属性1
2
3
4
5
6
7
8
9
10
11
12
13JNIEXPORT void JNICALL Java_com_chs_JniTest_accessStaticField
(JNIEnv *env, jobject jobj){
//jclass
jclass cls = (*env)->GetObjectClass(env, jobj);
//jfieldID
jfieldID fid = (*env)->GetStaticFieldID(env, cls, "count", "I");
//GetStatic<Type>Field
jint count = (*env)->GetStaticIntField(env, cls, fid);
count++;
//修改
//SetStatic<Type>Field
(*env)->SetStaticIntField(env,cls,fid,count);
}
3.访问java方法
这里需要用到方法的签名,方法的签名的获取,可以通过javap命令,打开命令行,进入我们的java类所在的文件夹,执行javap命令 比如1
javap -s -p com.chs.JniTest
执行上面的命令,就可以看到该类下面所有属性和方法的签名了。1
2
3
4
5
6
7
8
9JNIEXPORT void JNICALL Java_com_chs_JniTest_accessMethod
(JNIEnv *env, jobject jobj){
//jclass
jclass cls = (*env)->GetObjectClass(env, jobj);
//jmethodID 最后一个参数是方法的签名
jmethodID mid = (*env)->GetMethodID(env, cls, "genRandomInt", "(I)I");
//Call<Type>Method
jint random = (*env)->CallIntMethod(env, jobj, mid, 200);
printf("random num:%ld",random);
4.访问静态方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21JNIEXPORT void JNICALL Java_com_chs_JniTest_accessStaticMethod
(JNIEnv *env, jobject jobj){
//jclass
jclass cls = (*env)->GetObjectClass(env, jobj);
//jmethodID
jmethodID mid = (*env)->GetStaticMethodID(env, cls, "getUUID", "()Ljava/lang/String;");
//CallStatic<Type>Method
jstring uuid = (*env)->CallStaticObjectMethod(env, cls, mid);
//随机文件名称 uuid.txt
//jstring -> char*
//第三个参数isCopy JNI_FALSE,代表java和c操作的是同一个字符串
char *uuid_str = (*env)->GetStringUTFChars(env, uuid, NULL);
//拼接
char filename[100];
sprintf(filename, "D://%s.txt",uuid_str);
FILE *fp = fopen(filename,"w");
fputs("i love jason", fp);
fclose(fp);
}
5.访问构造方法
访问构造方式的时候需要传入一个”
1 | //使用java.util.Date产生一个当前的时间戳 |
6.调用父类的方法
Java中定义一个父类Human,和一个子类Man,父类中有个方法sayHai,子类重写该方法。
java中
1 | Human human = new Man(); |
C中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17JNIEXPORT void JNICALL Java_com_chs_JniTest_accessNonvirtualMethod
(JNIEnv *env, jobject jobj){
//拿到该对象
jclass cls = (*env)->GetObjectClass(env, jobj);
//找到属性的ID
jfieldID fid = (*env)->GetFieldID(env, cls, "human", "Lcom/chs/Human;");
//获取man属性(对象
jobject human_obj = (*env)->GetObjectField(env, jobj, fid);
//知道父类,注意:传父类的名称
jclass human_cls = (*env)->FindClass(env, "com/chs/Human");
//执行sayHi方法
jmethodID mid = (*env)->GetMethodID(env, human_cls, "sayHi", "()V");
//执行CallObjectMethod,还是会调用子类的方法
//(*env)->CallObjectMethod(env, human_obj, mid);
//使用CallNonvirtualObjectMethod可以调用的父类的方法
(*env)->CallNonvirtualObjectMethod(env, human_obj, human_cls, mid);
}
7.从C中返回中文乱码问题
C中字符串默认是UTF-16,想要返回到Java中不乱码,需要进行转码,C中转码非常麻烦,通过C调用Java中的String提供的类来进行转码就简单了很多。
1 | JNIEXPORT jstring JNICALL Java_com_chs_JniTest_chineseChars |
8.Java传数组到C中
传入一个数组并排序,操作完一定要同步回去1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//比较的方法
int compare(int *a,int *b){
return (*a) - (*b);
}
//传入
JNIEXPORT void JNICALL Java_com_chs_JniTest_giveArray
(JNIEnv *env, jobject jobj, jintArray arr){
//jintArray -> jint指针 -> c int 数组
jint *elems = (*env)->GetIntArrayElements(env, arr, NULL);
//printf("%#x,%#x\n", &elems, &arr);
//数组的长度
int len = (*env)->GetArrayLength(env, arr);
//排序
qsort(elems, len, sizeof(jint), compare);
//同步
//0, Java数组进行更新,并且释放C/C++数组
//JNI_ABORT, Java数组不进行更新,但是释放C/C++数组
//JNI_COMMIT,Java数组进行更新,不释放C/C++数组(函数执行完,数组还是会释放)
(*env)->ReleaseIntArrayElements(env, arr, elems, JNI_COMMIT);
}
9.C返回数组到Java
操作完一定要同步回去
1 | JNIEXPORT jintArray JNICALL Java_com_chs_JniTest_getArray(JNIEnv *env, jobject jobj, jint len){ |
10.JNI中的引用
引用类型分为局部引用和全局引用
局部引用可以使用DeleteLocalRef手动释放对象,什么时候释放呢
- 当访问一个恨到的java对象,使用完成知乎,还要进行复杂的耗时操作
- 当创建了大量的局部引用,占用了太多的内存,这些局部引用只是暂时使用,跟后面的操作没有关联。
局部引用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15JNIEXPORT void JNICALL Java_com_chs_JniTest_localRef(JNIEnv *env, jobject jobj){
int i = 0;
for (; i < 5; i++){
//创建Date对象
jclass cls = (*env)->FindClass(env, "java/util/Date");
jmethodID constructor_mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
jobject obj = (*env)->NewObject(env, cls, constructor_mid);
//省略一些操作
//不在使用jobject对象了
//通知垃圾回收器回收这些对象
(*env)->DeleteLocalRef(env, obj);
//省略一些操作...
}
}
全局引用
全局引用可以共享数据,可以跨多个线程,使用DeleteGlobalRef释放。
1 | jstring global_str; |
弱全局引用
全局引用需要手动释放,弱全局引用,跟java中的弱引用差不多,当内存不足的时候系统会释放掉这部分内存。
创建:NewWeakGlobalRef,销毁:DeleteGlobalWeakRef
11.C中异常处理
- C中发生错误抛出的异常,可以使用Throwable捕获,可以从C中把异常清理后才能继续执行后面的代码。C中使用
ExceptionOccurred
查看返回的异常是不是NULL。不过不是,可以清空异常,并做一些补救的措施。 - C中通过手动通过ThrowNew抛出的异常,可以在java层try catch捕获到。
1 | JNIEXPORT void JNICALL Java_com_chs_JniTest_exeception(JNIEnv *env, jobject jobj){ |
12.缓存策略
如果从java中循环调用很多次下面的方法,我们让对象第一次拿到之后就缓存起来,以后再取的时候就去缓存中取,这时候可以使用局部静态变量来存储。
在实际的项目中,我们也可以在一个方法中一次初始化很多需要的全局的变量。
1 | JNIEXPORT void JNICALL Java_com_chs_JniTest_cached(JNIEnv *env, jobject jobj){ |