本文来说下JNI的几个问题
文章目录
JNI 概述
JNI是Java Native Interface的缩写,JAVA本地调用。从jdk1.1开始,Java Native Interface(JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。看过JDK源代码的人肯定会注意到在源码里有很多标记成native的方法。这些个方法只有方法签名但是没有方法体。其实这些naive方法就是我们说的 java native interface。他提供了一个调用(invoke)的接口,然后用C或者C++去实现;
JNI是JAVA标准平台中的一个重要功能,它弥补了JAVA的与平台无关这一重大优点的不足,在JAVA实现跨平台的同时,也能与其它语言(如C、C++)的动态库进行交互,给其它语言发挥优势的机会。
在前面介绍UVM的文章中,简单的提到了java中的本地方法(Native Method),它可以通过JNI(Java Native Interface)调用其他语言中的函数来实现一些相对底层的功能,本文我们就来顺藤摸瓜,介绍一下jni以及它的使用。
首先回顾一下jni的主要功能,从jdk1.1开始jni标准就成为了java平台的一部分,它提供的一系列的API允许java和其他语言进行交互,实现了在java代码中调用其他语言的函数。通过jni的调用,能够实现这些功能:
通常情况下我们一般使用jni用来调用c或c++中的代码,在上一篇文章中我们用了下面的流程来描述了native方法的调用过程:
Java Code -> JNI -> C/C++ Code
但是准确的来说这一过程并不严谨,因为最终被执行的不是原始的c/c++代码,而是被编译连接后的动态链接库。因此我们将这个过程从单纯的代码调用层面上进行升级,将jni的调用过程提高到了jvm和操作系统的层面,来加点细节进行一下完善:
看到这里,可能有的小伙伴就要提出疑问了,不是说java语言是跨平台的吗,这种与操作系统本地编译的动态链接库进行的交互,会不会使java失去跨平台的可移植性?
针对这一问题,大家可以回想一下以前安装jdk的经历,在官网的下载列表中提供了各个操作系统的不同版本jdk,例如windows、linux、mac os版本等等,在这些jdk中,针对不同系统有着不同的jvm实现。而java语言的跨平台性恰好是和它底层的jvm密不可分的,正是依靠不同的操作系统下不同版本jvm的“翻译”工作,才能使编译后的字节码在不同的平台下畅通无阻的运行。
在不同操作系统下,c/c++或其他代码生成的动态链接库也会有差异,例如在window平台下会编译为dll文件,在linux平台下会编译为so文件,在mac os下会编译为jnilib文件。而不同平台下的jvm,会“约定俗成”的去加载某个固定类型的动态链接库文件,使得依赖于操作系统的功能可以被正常的调用,这一过程可以参考下面的图来进行理解:
在对jni的整体调用流程有了一定的了解后,对于它如何调用其他语言中的函数这一过程,你是否也会好奇它是怎样实现的,下面我们就通过手写一个java程序调用c++代码的例子,来理解它的调用过程。
Java 代码和JNI代码通信
Java代码通过JNI接口 调用 C/C++方法
1、首先我们需要在Java代码中声明Natvie方法原型
public native void helloJNI(String msg);
2、其次我们需要在C/C++代码里声明JNI方法的原型
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_helloJNI(JNIEnv* env, jobject thiz,jstring msg) {
//do something
}
- extern “C”。JNI函数声明声明代码是用C++语言写的,所以需要添加extern "C"声明;如果源代码是C语言声明,则不需要添加这个声明
- JNIEXPORT。这个关键字表明这个函数是一个可导出函数。每一个C/C++库都有一个导出函数列表,只有在这个列表里面的函数才可以被外部直接调用,类似Java的public函数和private函数的区别。
- JNICALL。说明这个函数是一个JNI函数,用来和普通的C/C++函数进行区别。
- Void 返回值类型
- JNI函数名原型:Java_ + JNI方法所在的完整的类名,把类名里面的”.”替换成”_” + 真实的JNI方法名,这个方法名要和Java代码里面声明的JNI方法名一样。
- env 参数 是一个执行JNIENV函数表的指针。
- thiz 参数 代表的是声明这个JNI方法的Java类的引用。
- msg 参数就是和Java声明的JNI函数的msg参数对于的JNI函数参数
静态JNI方法和实例JNI方法的区别
Java代码:
public native void showHello();
public native static void showHello2();
C++代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_showHello(JNIEnv* env, jobject thiz) {
//do something
}
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_showHello2(JNIEnv* env, jclass thiz) {
//do something
}
java和JNI类型对照表
Java和JNI基本类型对照表
java的基本类型可以直接与C/C++的基本类型映射。
Java与JNI引用类型对照表
与Java基本类型不同,引用类型对开发人员是不透明的。Java内部数据结构并不直接向原生代码开放。也就是说 C/C++代码并不能直接访问Java代码的字段和方法
JNI基本操作举例
JNI操作字符串
java 类 TestNatvie.java
/**
* 字符串相关测试代码
* @param str
*/
public native void testJstring(String str);
C++文件 natvie-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testJstring(JNIEnv *env, jobject instance,
jstring str_) {
//(1)生成JNI String
char const * str = "hello world!";
jstring jstring = env->NewStringUTF(str);
// (2) jstring 转换成 const char * charstr
const char *charstr = env->GetStringUTFChars(str_, 0);
// (3) 释放 const char *
env->ReleaseStringUTFChars(str_, charstr);
//(4) 获取字符串子集
char * subStr = new char;
env->GetStringUTFRegion(str_,0,3,subStr);//截取字符串char*;
env->ReleaseStringUTFChars(str_, subStr);
}
JNI操作数组
java 类 TestNatvie.java
/**
* 整形数组相关代码
* @param array
*/
public native void testIntArray(int []array);
/**
*
* Object Array 相关测试 代码
* @param strArr
*/
public native void testObjectArray(String[]strArr);
C++文件 natvie-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testIntArray(JNIEnv *env, jobject instance,
jintArray array_) {
//----获取数组元素
//(1)获取数组中元素
jint * intArray = env->GetIntArrayElements(array_,NULL);
int len = env->GetArrayLength(array_);//(2)获取数组长度
LOGD("feifei len:%d",len);
for(int i = 0; i < len;i++){
jint item = intArray[i];
LOGD("feifei item[%d]:%d",i,item);
}
env->ReleaseIntArrayElements(array_, intArray, 0);
//----- 获取子数组
jint *subArray = new jint;
env->GetIntArrayRegion(array_,0,3,subArray);
for(int i = 0;i<3;i++){
subArray[i]= subArray[i]+5;
LOGD("feifei subArray:[%d]:",subArray[i]);
}
//用子数组修改原数组元素
env->SetIntArrayRegion(array_,0,3,subArray);
env->ReleaseIntArrayElements(array_,subArray,0);//释放子数组元素
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testObjectArray(JNIEnv *env, jobject instance,
jobjectArray strArr) {
//获取数组长度
int len = env->GetArrayLength(strArr);
for(int i = 0;i< len;i++){
//获取Object数组元素
jstring item = (jstring)env->GetObjectArrayElement(strArr,i);
const char * charStr = env->GetStringUTFChars(item, false);
LOGD("feifei strArray item:%s",charStr);
jstring jresult = env->NewStringUTF("HaHa");
//设置Object数组元素
env->SetObjectArrayElement(strArr,i,jresult);
env->ReleaseStringUTFChars(item,charStr);
}
}
JNI访问Java类的方法和字段
JNI 中访问java类的方法和字段都是 通过反射来实现的。
JNI获取Java类的方法ID和字段ID,都需要一个很重要的参数,就是Java类的方法和字段的签名
JNI 中访问Java对象的属性 和方法:
java 类 TestNatvie.java
public class TestNatvie {
static {
System.loadLibrary("native-lib");
}
/**
* Jni调用 java 对象方法
*/
public native void testCallJavaMethod();
/**
* Jni 调用 java static 方法
*/
public native void testCallStaticJavaMethod();
/**
* JNI 访问 java 的对象属性和类属性
* @param student
*/
public native void getJavaObjectField(Student student);
public void helloworld(String msg){
Log.d("feifei","hello world:"+msg);
}
public static void helloworldStatic(String msg){
Log.d("feifei","hello world:"+msg);
}
}
C++ 类 natvie-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testCallJavaMethod(JNIEnv *env, jobject instance) {
//获取类名
jclass clazz = env->GetObjectClass(instance);
if(clazz == NULL) return;
jmethodID javaMethod = env->GetMethodID(clazz,"helloworld","(Ljava/lang/String;)V");
if(javaMethod == NULL)return;
const char * msg = "nancy";
jstring jmsg = env->NewStringUTF(msg);
env->CallVoidMethod(instance,javaMethod,jmsg);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testCallStaticJavaMethod(JNIEnv *env, jobject instance) {
//获取java类型
jclass clazz = env->GetObjectClass(instance);
if(clazz == NULL) return;
jmethodID staticMethod = env->GetStaticMethodID(clazz,"helloworldStatic","(Ljava/lang/String;)V");
if(staticMethod == NULL) return;
jstring jmsg = env->NewStringUTF("wangfeng");
env->CallStaticVoidMethod(clazz,staticMethod,jmsg);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_getJavaObjectField(JNIEnv *env, jobject instance,
jobject student) {
jclass clazz = env->GetObjectClass(student);
if(clazz == NULL )return;
// 获取Object 实例属性
jfieldID nameId = env->GetFieldID(clazz,"name","Ljava/lang/String;");
jstring jname = (jstring)env->GetObjectField(student,nameId);
jfieldID ageId = env->GetFieldID(clazz,"age","I");
jint jage = env->GetIntField(student,ageId);
const char * name = env->GetStringUTFChars(jname,false);
env->ReleaseStringUTFChars(jname,name);
//获取java 类属性:
jfieldID gradeId = env->GetStaticFieldID(clazz,"grade","I");
jint jgrade = env->GetStaticIntField(clazz,gradeId);
jfieldID nickeNameID = env->GetStaticFieldID(clazz,"nickname","Ljava/lang/String;");
jstring jnickname = (jstring)env->GetStaticObjectField(clazz,nickeNameID);
const char * nickeName = env->GetStringUTFChars(jnickname, false);
env->ReleaseStringUTFChars(jnickname,nickeName);
LOGD("feifei name:%s,age:%d,grade:%d,nickname:%s",name,jage,jgrade,nickeName);
//JNI 设置 java对象属性
env->SetObjectField(student,nameId,env->NewStringUTF("张三"));
//JNI 设置 java 类属性
env->SetStaticObjectField(clazz,nickeNameID,env->NewStringUTF("小白"));
jstring jnameNew = (jstring)env->GetObjectField(student,nameId);
jstring jnickNameNew = (jstring)env->GetStaticObjectField(clazz,nickeNameID);
const char * newName = env->GetStringUTFChars(jnameNew, false);
const char *newNickName = env->GetStringUTFChars(jnickNameNew, false);
env->ReleaseStringUTFChars(jnameNew,newName);
env->ReleaseStringUTFChars(jnickNameNew,newName);
LOGD("feifei after update name:%s,age:%d,grade:%d,nickname:%s",newName,jage,jgrade,newNickName);
}
JNI对象的全局引用和局部引用
Java代码的内存是由垃圾回收器来管理,而JNI代码则不受Java的垃圾回收器来管理。所以JNI代码提供了一组函数,来管理通过JNI代码生成的JNI对象,比如jobject,jclass,jstring,jarray等。
JNI对象的局部引用
在JNI接口函数中引用JNI对象的局部变量,都是对JNI对象的局部引用,一旦JNI接口函数返回,所有这些JNI对象都会被自动释放。不过我们也可以采用JNI代码提供的DeleteLocalRef函数来删除一个局部JNI对象引用。
//声明局部变量clazz
jclass clazz = env->GetObjectClass(instance);
//手动释放 局部变量 clazz ;DeleteLocalRef 也可不用手动调用,JNI方法返回之后,会自动释放局部JNI变量
env->DeleteLocalRef(clazz);
JNI对象的全局引用
JNI对象的全局引用分为两种,一种是强全局引用,这种引用会阻止Java的垃圾回收器回收JNI代码引用的Java对象,另一种是弱全局引用,这种全局引用则不会阻止垃圾回收器回收JNI代码引用的Java对象。
1、强全局引用
- NewGlobalRef用来创建强全局引用的JNI对象
- DeleteGlobalRef用来删除强全局引用的JNI对象
2、弱全局引用
- NewWeakGlobalRef用来创建弱全局引用的JNI对象
- DeleteWeakGlobalRef用来删除弱全局引用的JNI对象
- IsSameObject用来判断两个JNI对象是否相同
Java类 TestNatvie.java
/**
* 测试 JNI 强全局引用 和弱全局引用
*/
public native void testJNIReference(Object object);
C++ 代码 natvie-lib.cpp
/**
* (1)在JNI接口函数中引用JNI对象的局部变量,都是对JNI对象的局部引用,一旦JNI接口函数返回,所有这些JNI对象都会被自动释放。不过我们也可以采用JNI代码提供的DeleteLocalRef函数来删除一个局部JNI对象引用
* (2)对于JNI对象,绝对不能简单的声明一个全局变量,在JNI接口函数里面给这个全局变量赋值这么简单,一定要使用JNI代码提供的管理JNI对象的函数.
* JNI 全局引用分为两种: 一种全局引用,这种引用会阻止Java垃圾回收器回收JNI代码引用的对象;
* 另一种是弱全局引用,这种全局引用不会阻止垃圾回收器回收JNI 代码引用的Java对象
* - NewGlobalRef用来创建强全局引用的JNI对象
* - DeleteGlobalRef用来删除强全局引用的JNI对象
* - NewWeakGlobalRef用来创建弱全局引用的JNI对象
* - DeleteWeakGlobalRef用来删除弱全局引用的JNI对象
* - IsSameObject用来判断两个JNI对象是否相同
*/
jobject gThiz; //全局JNI对象引用
jobject gWeakThiz;//全局JNI对象弱应用
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testJNIReference(JNIEnv *env, jobject instance,jobject obj) {
//声明局部变量clazz
jclass clazz = env->GetObjectClass(instance);
//手动释放 局部变量 clazz ;DeleteLocalRef 也可不用手动调用,JNI方法返回之后,会自动释放局部JNI变量
env->DeleteLocalRef(clazz);
//---- 强全局变量
gThiz = env->NewGlobalRef(obj);//生成全局的JNI 对象引用,这样生成的全局的JNI对象 才可以在其他函数中使用
env->DeleteGlobalRef(gThiz);//在我们不需要gThis这个全局JNI对象应用时,可以将其删除。
//---- 全局弱引用
gWeakThiz = env->NewWeakGlobalRef(obj);//生成全局的JNI对象引用,这样生成的全局的JNI对象才可以在其它函数中使用
if(env->IsSameObject(gWeakThiz,NULL)){
LOGD("全局弱引用 已经被释放了");
}
//释放 全局弱应用对象
env->DeleteWeakGlobalRef(gWeakThiz);
}
JNI进程间同步
JNI可以使用Java对象进行线程同步
- MonitorEnter函数用来锁定Java对象
- MonitorExit函数用来释放Java对象锁
Java类 TestNative.java
/**
* JNI 利用 java 对象进行线程同步
* @param lock
*/
public native void testJNILock(Object lock);
C++ 类 native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testJNILock(JNIEnv *env, jobject instance,
jobject lock) {
//加锁
env->MonitorEnter(lock);
//doSomething
LOGD("feifei, this is in lock");
//释放锁
env->MonitorExit(lock);
}
JNI异常相关的函数
当JNI函数调用的Java方法出现异常的时候,并不会影响JNI方法的执行,但是我们并不推荐JNI函数忽略Java方法出现的异常继续执行,这样可能会带来更多的问题。我们推荐的方法是,当JNI函数调用的Java方法出现异常的时候,JNI函数应该合理的停止执行代码。
- ExceptionOccurred函数用来判断JNI函数调用的Java方法是否出现异常
- ExceptionClear函数用来清除JNI函数调用的Java方法出现的异常
/**
* 1、env->ExceptionOccurred() 判断JNI调用java方法 是否遇到了Exception
* 2、env->ThrowNew() JNI 可以主动抛出Java Exception异常
*/
public native void testJavaException();
C++ 类 native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testJavaException(JNIEnv *env, jobject instance) {
jclass clazz = env->GetObjectClass(instance);
if(clazz == NULL) return;
jmethodID helloException_method = env->GetMethodID(clazz,"helloException","()V");
if(helloException_method == NULL )return;
env->CallVoidMethod(instance,helloException_method);
if(env->ExceptionOccurred() != NULL){
// env->ExceptionDescribe();
env->ExceptionClear();
LOGD("feifei,调用java 方法时 遇到了Exception");
return;
}
LOGD("feifei,调用helloException 方法成功了!");
LOGD("feifei,now JNI throw java exception - beging");
jclass expetionClazz = env->FindClass("java/lang/Exception");
if(expetionClazz == NULL) return;
env->ThrowNew(expetionClazz,"this is a exception");
}
JNI抛出Java类型的异常
JNI通过ThrowNew函数抛出Java类型的异常
Java类 TestNative.java
LOGD("feifei,now JNI throw java exception - beging");
jclass expetionClazz = env->FindClass("java/lang/Exception");
if(expetionClazz == NULL) return;
env->ThrowNew(expetionClazz,"this is a exception");
JNI和Java对象的互相持有
Java对象持久化C/C++对象实例
通常的做法是 将C++对象指针 强转为jlong 类型,保存在调用者java对象的long型变量中,一直持有。当需要使用该C++对象时,从Java对象中的long变量,强转化为C++对象,进而使用。
TestNative.java
public class TestNatvie {
static {
System.loadLibrary("native-lib");
}
/**
* 用户保存 C++对象的引用
*/
private long mNatvieId;
/**
* Java 对象持有 C++对象
*/
public native void initSDK();
/**
* Java 对象释放 C++对象
*/
public native void releasSDK();
}
native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_initSDK(JNIEnv *env, jobject instance) {
Person * person = new Person();
person->setAge(18);
person->initSDK();
jclass classzz = env->GetObjectClass(instance);
jfieldID fid = env->GetFieldID(classzz,"mNatvieId","J");
//将C++对象的地址绑定到Java变量中
env->SetLongField(instance,fid,(jlong)person);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_releasSDK(JNIEnv *env, jobject instance) {
jclass objectClass = env->GetObjectClass(instance);
jfieldID fid = env->GetFieldID(objectClass,"mNatvieId","J");
//取出java对象中保存的C++对象地址
jlong p = env->GetLongField(instance,fid);
//转换成 C++对象
Person * person = (Person*)p;
person->releaseSDK();
//释放person C++对象
free(person);
env->SetLongField(instance,fid,-1);
}
C/C++持久化Java对象
在本地方式中,创建一个全局引用 保存java对象:
env->NewGlobalRef(obj);
这样在其他的JNI方法中就可以任意的使用该java对象了。
在不需要改java对象时,再将JNI全局引用删除即可。
env->DeleteGlobalRef(gThiz);
使用示例:
TestNative.cpp
/**
* 利用JNI全局引用持有java 对象
*/
public native void testJNIReference(Object object);
natvie-lib.cpp
jobject gThiz; //全局JNI对象引用 - 用于持有特定的java对象。
jobject gWeakThiz;//全局JNI对象弱应用
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testJNIReference(JNIEnv *env, jobject instance,jobject obj) {
//---- 强全局变量
gThiz = env->NewGlobalRef(obj);//生成全局的JNI 对象引用,这样生成的全局的JNI对象 才可以在其他函数中使用
env->DeleteGlobalRef(gThiz);//在我们不需要gThis这个全局JNI对象应用时,可以将其删除。
}
本文小结
本文详细介绍了JNI相关的知识与内容。