Java Native Interface,Java调用本地方法的技术,简单来说,当Java运行在Windows平台时,通过JNI和Windows底层也可以理解为和 C/C++ 进行交互。Jvm就是通过大量的JNI技术使得Java能够在不同平台上运行。
javac xxx.java //生成 .class 文件
javah xxx.xxx(全类名) //生成 .h 头文件
javac -h . xxx.java //Java1.8 以上 代替上面两个命令 生成 .class .h 文件
javap -s -p xxx.class//查看类中的字段和方法的签名
Java类型 | 签名 |
---|---|
byte | B |
short | S |
int | I |
long | J |
float | F |
double | D |
boolean | Z |
char | C |
void | V |
方法的签名写法:(参数签名)返回值类型签名
如 : 方法public int test(int i, String s, long[] l){ ... }
所对应的签名就是(ILjava/lang/String;[J)I
,方法的签名也可以通过命令行 javap -s -p xxx.class
去查看;
JNI类型 | Java类型 |
---|---|
jbyte | byte |
jshort | short |
jint | int |
jlong | long |
jfloat | float |
jdouble | double |
jboolean | boolean |
jchar | char |
void | void |
JNI类型 | Java类型 |
---|---|
jclass | Class |
jobject | Object |
jstring | String |
jobejctArray | Object[] |
jbyteArray | byte[] |
jshortArray | short[] |
jintArray | int[] |
jlongArray | long[] |
jdoubleArray | double[] |
jbooleanArray | boolean[] |
jcharArray | char[] |
jthrowable | Throwable |
静态库:这类库的名字一般是 xxx.a ;利用静态函数库编译的文件较大,整个函数库所有的数据都会被整合进目标代码中;优点,编译后执行程序不需要外部的函数库支持;缺点,如果静态函数库改变了,需要重新编译。
动态库:这类库的名字一般是 xxx.so ;相比于静态库,在编译时并没有整合进目标代码,在程序执行到相关函数时才调用对应函数库的函数,因此生成的可执行文件较小。运行环境必须提供对应的库,动态函数库的改变不影响程序,动态库升级比较方便。
新建 StaticReg.java 文件
public class StaticReg {
// c/c++ 层要实现的方法
public native void Hello();
public static void main(String[] args) {
}
}
进入到StaticReg.java所在的目录中,通过命令行生成 .class .h 文件:
javac -h . StaticReg.java
打开Clion,新建一个C++ Library项目
新建项目之后,将上一步生成的 .h 文件复制到 C 项目中,并且以同样的文件名新建一个 .c 文件,实现里面的函数
这两个参数代表的含义:
JNIEnv* env参数:实质上代表Java 环境,通过这个指针,就可以对Java端代码进行操作,创建Java类的对象,调用Java对象方法,获取Java对象属性等;
jobject obj参数:如果native 方法是 static,那么这个 obj 就代表这个native的实例;如果native方法不是 static,那么这个 obj 就代表native方法的类的class对象实例;
编写完成之后,在CMakeLists.txt 中添加以下代码:
## staticReg 要生成的动态库文件名
## SHARED 库的类型
## 后面的.c .h 文件 是指要包含的源文件
add_library(staticReg SHARED com_shy_sample_jniReg_StaticReg.c com_shy_sample_jniReg_StaticReg.h)
添加完成之后编译项目
编译完成后会在目录下生成 这么俩个文件, .dll 文件就是在Windows平台上生成的动态库,在Linux平台与之对应的就是 .so 库
回到Java代码中,StaticReg.java中添加以下代码:
public class StaticReg {
static {
//引入 C 编译出来的 .dll 文件
System.load("E:\\CProject\\study_jni_reg\\cmake-build-debug\\libstaticReg.dll");
}
public native void Hello();
public static void main(String[] args) {
StaticReg reg = new StaticReg();
reg.Hello();
}
}
首先,新建Java文件,DynamicReg.java
public class DynamicReg {
public native void sayHello();
public native void getRandom();
public static void main(String[] args) {
}
}
和静态注册不同的是,我们不再需要去编译头文件等,直接再C 项目中 新建 DynamicReg.c 文件,代码中有详细注释:
#include "jni.h"
//这两个方法 分别对应 Java中定义的两个 native方法
void sayHello(JNIEnv *env, jobject jobj){
printf("JNI -> say Hello ! \n");
}
jint getRandom(JNIEnv *env, jobject jobj){
return 666;
}
// Java 类的 全类名
static const char * mClassName = "com/shy/sample/jniReg/DynamicReg";
//存放JNINativeMethod结构体的数组,
//结构体三个参数分别代表: java中native方法名, 方法签名, C中对应的方法指针
static const JNINativeMethod mMethods[] = {
{"sayHello", "()V", (void*)sayHello},
{"getRandom", "()I",(void*)getRandom},
};
//JNI_OnLoad 方法 在Java 端调用System.load后会执行
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
printf("JNI_OnLoad start _______________\n");
JNIEnv* env = NULL;
//获得 JniEnv
int r = (*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4);
if( r != JNI_OK){
return -1;
}
jclass mainActivityCls = (*env)->FindClass(env, mClassName);
// 注册 如果小于0则注册失败
// 一定要注意 RegisterNatives 最后一个参数,代表方法个数
r = (*env)->RegisterNatives(env,mainActivityCls,mMethods,2);
if(r != JNI_OK )
{
return -1;
}
printf("JNI_OnLoad end __________________\n");
return JNI_VERSION_1_4;
}
上述代码中:
sayHello 和 getRandom 分别对应Java 代码中定义的两个native方法;
mClassName ,Java中的类的全类名;
mMethods,一个数组,存放的是 JNINativeMethod 结构体的元素,这个数组主要是匹配 C 和 Java 两端的方法;
JNI_OnLoad 方法,当Java中执行System.load时,会执行这个方法,这个方法也是动态注册的关键方法;
然后编译项目,生成 .dll 和 .dll.a 文件:
回到Java 端,修改DynamicReg.java代码:
public class DynamicReg {
static {
System.load("E:\\CProject\\study_jni_reg\\cmake-build-debug\\libdynamicReg.dll");
}
public native void sayHello();
public native int getRandom();
public static void main(String[] args) {
DynamicReg dynamicReg = new DynamicReg();
dynamicReg.sayHello();
System.out.println("返回结果: " + dynamicReg.getRandom());
}
}
运行结果:
动态注册相比于静态注册,省去了我们手动编译java文件,导入.h头文件的过程,在JNI_OnLoad 方法中帮我们匹配了方法调用;
在上面的例子中,已经完成了Java 通过 JNI 调用 C/C++,很多时候我们在C/C++中也需要获取Java类中的变量,对他们进行一系列操作,下面就来实现 C/C++ 中获取 Java 类中的变量
新建一个 Test.java 文件
public class Test {
// 这个要在C 项目编译后,生成 .dll 文件之后 再加载这个文件 我这里提前写上了
static {
System.load("E:\\CProject\\study_jni_reg\\cmake-build-debug\\libchangeNum.dll");
}
int num = 1;
static int staticNum = 100;
String name = "Sunhy";
public native void changeNum();
public native void changeStaticNum();
public native String sayHello(String str);
public static void main(String[] args) {
Test test = new Test();
test.changeNum();
test.changeStaticNum();
System.out.println("num = " + test.num);
System.out.println("staticNum = " + staticNum);
System.out.println("sayHello -> " + test.sayHello(test.name));
}
}
Test.java中,定义了普通变量、静态变量、有返回值的native函数,下面具体来实现一下C/C++访问普通变量、静态变量以及返回给Java层返回值。
首先在C 项目中创建 ChangeNum.c 文件,导入头文件#include "jni.h"
,并且对应实现Java中的方法,采用静态注册,所以方法名用 全类名+方法名 来对应
#include "jni.h"
#include
#include
#include
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_changeNum
(JNIEnv* env, jobject jobj){
}
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_changeStaticNum
(JNIEnv* env, jobject jobj){
}
JNIEXPORT jstring JNICALL Java_com_shy_sample_jniField_Test_sayHello
(JNIEnv* env, jobject jobj, jstring str){
}
先编写访问普通变量的方法Java_com_shy_sample_jniField_Test_changeNum,获取到Java类中的num变量,并且修改它:
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_changeNum
(JNIEnv* env, jobject jobj){
// 1.获取类
jobject clz = (*env)->GetObjectClass(env, jobj);
// 2.获取属性的ID 最后一个参数是变量的签名
jfieldID numId = (*env)->GetFieldID(env, clz, "num", "I");
// 3.获取变量的值
jint num = (*env)->GetIntField(env, clz, numId);
printf("JNI -> C -> num = %d\n", num);
// 4.修改变量的值
(*env)->SetIntField(env, clz, numId, 1000 + num);
}
这就完成了对Java类中普通变量num的值的修改
访问静态变量和访问普通变量流程是一样的,只不过每一步调用的方法不同,编写Java_com_shy_sample_jniField_Test_changeStaticNum方法:
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_changeStaticNum
(JNIEnv* env, jobject jobj){
//获取类的方法有两种 FindClass 需要传入类的全类名
//jobject clz = (*env)->FindClass(env, "com/shy/sample/jniField/Test");
jobject clz = (*env)->GetObjectClass(env, jobj);
jfieldID staticNumId = (*env)->GetStaticFieldID(env, clz, "staticNum", "I");
jint staticNum = (*env)->GetStaticIntField(env, clz, staticNumId);
printf("JNI -> C -> staticNum = %d\n", staticNum);
(*env)->SetStaticIntField(env, clz, staticNumId, 1000 + staticNum);
}
访问静态变量,调用的都是GetStaticXXX 或者 SetStaticXXX;
前面的例子中,都是无返回值void类型的native函数,这里通过实现Java类中的sayHello(String str),来实现接受Java传递的参数,并且返回值给Java:
JNIEXPORT jstring JNICALL Java_com_shy_sample_jniField_Test_sayHello
(JNIEnv* env, jobject jobj, jstring str){ //注意这里,Java传递的参数这里要对应
jboolean iscp;
// 1. 先获取到 java 端传过来的参数
const char* name = (*env) -> GetStringUTFChars(env, str, &iscp);
// 2. 定义一个字符数组
char buf[128] = {0};
// 3. 拼接字符数组
sprintf(buf, "Hello --->> %s", name);
// 4. 释放资源
(*env) -> ReleaseStringUTFChars(env, str, name);
// 5. 返回
return (*env) -> NewStringUTF(env, buf);
}
编译C 项目,生成 .dll 文件,运行Java代码,运行结果:
这里我们会发现,打印的日志顺序反了,应该 下面两句 JNI 开头的先打印,因为他们在C 的方法中;这是因为,C/C++ 和 Java 分别有自己的缓冲区,每次刷新缓冲区,C/C++才能将标准输出送到Java的控制台。
C/C++ 可以访问 Java中的变量,那么肯定也能调用Java中的方法,这种场景经常用于,C/C++ 需要创造返回一个Java对象时使用,如需要返回一个Bitmap时,那么就需要在C/C++ 层调用对应Java方法去实现。
C/C++ 调用Java方法,主要区分为 调用构造方法、非静态方法、静态方法。首先,在Java端新建一个JNICall的类:
public class JNICall {
// 构造方法
public JNICall(){
System.out.println("JNICall -> Constructor is be invoked ");
}
// 普通方法
public void JNICallMethod(){
System.out.println("JNICall -> Method is be invoked ");
}
// 静态方法
public static void JNICallStaticMethod(){
System.out.println("JNICall -> Static method is be invoked ");
}
}
接着,继续使用上面例子中的Test.java,在其中定义三个native方法:
public class Test {
//。。。多余代码省略
//在C/C++端实现下面的三个方法,去调用JNICall.java中的方法
public native void callConstructor();
public native void callMethod();
public native void callStaticMethod();
public static void main(String[] args) {
//。。。多余代码省略
Test test = new Test();
test.callConstructor();
test.callMethod();
test.callStaticMethod();
}
}
在C 项目中实现定义的三个方法,为了方便就直接写在上面定义的ChangNum.c 中:
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callConstructor
(JNIEnv* env, jobject jobj){
};
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callMethod
(JNIEnv* env, jobject jobj){
};
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callStaticMethod
(JNIEnv* env, jobject jobj){
};
下面就来分别实现三个方法
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callConstructor
(JNIEnv* env, jobject jobj){
// 1. 获取到要调用的类
jclass clz = (*env) -> FindClass(env, "com/shy/sample/jniField/JNICall");
// 2. 获取要调用的方法的ID 构造方法方法名必须传入
jmethodID methodId = (*env) -> GetMethodID(env, clz, "", "()V");
// 3. 创建 要调用类的 对象
jobject obj = (*env) -> NewObject(env, clz, methodId);
// 4. 调用
(*env) -> CallVoidMethod(env, obj, methodId);
};
调用构造方法,需要注意一点,方法名必须传入
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callMethod
(JNIEnv* env, jobject jobj){
// 1. 获取到要调用的类
jclass clz = (*env) -> FindClass(env, "com/shy/sample/jniField/JNICall");
// 2. 获取要调用的方法的ID
jmethodID methodId = (*env) -> GetMethodID(env, clz, "JNICallMethod", "()V");
// 3. 创建 要调用类的 对象
// 就如同java 中 new 对象一样,需要指定构造方法
jmethodID constructorId = (*env) -> GetMethodID(env, clz, "", "()V");
jobject obj = (*env) -> NewObject(env, clz, constructorId);
// 4. 调用
(*env) -> CallVoidMethod(env, obj, methodId);
};
调用普通方法,就和Java很像,需要知道调用哪个类,new出来它的对象,然后调用
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callStaticMethod
(JNIEnv* env, jobject jobj){
// 1. 获取到要调用的类
jclass clz = (*env) -> FindClass(env, "com/shy/sample/jniField/JNICall");
// 2. 获取要调用的方法的ID
jmethodID methodId = (*env) -> GetStaticMethodID(env, clz, "JNICallStaticMethod", "()V");
// 3. 调用
(*env) -> CallStaticVoidMethod(env, clz, methodId);
};
调用静态方法,也是和Java很像,在Java中静态方法是通过 类名.方法名 去调用的,所以,调用静态方法,就省去了new一个对象的操作。
上面的代码中,虽然功能都实现了,但是都存在内存泄漏,溢出的风险。在Java中有四种引用,分别是强、软、弱、虚引用,C语言中也存在三种引用:
(*env)->DeleteGlobalRef(env,g_cls_string);
(*env)->DeleteWeakGlobalRef(env,g_cls_string)
这就会出现一种情况:
JNIEXPORT jstring JNICALL Java_newString
(JNIEnv * env, jobject jobj){
// 定义静态的局部变量
static jclass cls_string = NULL;
if (cls_string == NULL) {
printf("cls_string is null \n");
cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
}
.....
}
上述代码中的 cls_string 是一个静态的局部变量,那么当方法执行一次后 静态变量cls_string 会指向 FindClass方法返回的局部引用的首地址,当函数执行结束,局部引用会失效,但是cls_string 中存放的是地址,当第二次执行该函数时,cls_string 不为NULL,也就不会执行 if 语句,从而导致它成为一个野指针;
所以在编写 JNI 时,一定要手动释放,在上述代码结束前把 cls_string 赋空值:
JNIEXPORT jstring JNICALL Java_newString
(JNIEnv * env, jobject jobj){
// 定义静态的局部变量
static jclass cls_string = NULL;
if (cls_string == NULL) {
printf("cls_string is null \n");
cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
}
.....
(*env)->DeleteLocalRef(env, cls_string);
cls_string = NULL;
}