Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)

什么是JNI

Java Native Interface,Java调用本地方法的技术,简单来说,当Java运行在Windows平台时,通过JNI和Windows底层也可以理解为和 C/C++ 进行交互。Jvm就是通过大量的JNI技术使得Java能够在不同平台上运行。

Java相关命令

 javac xxx.java  //生成 .class 文件
 javah xxx.xxx(全类名) //生成 .h 头文件
 javac -h . xxx.java //Java1.8 以上 代替上面两个命令 生成 .class .h 文件
 javap -s -p xxx.class//查看类中的字段和方法的签名	

Java 方法、变量的签名

Java类型签名
byteB
shortS
intI
longJ
floatF
doubleD
booleanZ
charC
voidV

方法的签名写法:(参数签名)返回值类型签名
如 : 方法public int test(int i, String s, long[] l){ ... } 所对应的签名就是(ILjava/lang/String;[J)I,方法的签名也可以通过命令行 javap -s -p xxx.class 去查看;

JNI 数据类型

基本类型
JNI类型Java类型
jbytebyte
jshortshort
jintint
jlonglong
jfloatfloat
jdoubledouble
jbooleanboolean
jcharchar
voidvoid
引用类型
JNI类型Java类型
jclassClass
jobjectObject
jstringString
jobejctArrayObject[]
jbyteArraybyte[]
jshortArrayshort[]
jintArrayint[]
jlongArraylong[]
jdoubleArraydouble[]
jbooleanArrayboolean[]
jcharArraychar[]
jthrowableThrowable

静态库 和 动态库

静态库:这类库的名字一般是 xxx.a ;利用静态函数库编译的文件较大,整个函数库所有的数据都会被整合进目标代码中;优点,编译后执行程序不需要外部的函数库支持;缺点,如果静态函数库改变了,需要重新编译。

动态库:这类库的名字一般是 xxx.so ;相比于静态库,在编译时并没有整合进目标代码,在程序执行到相关函数时才调用对应函数库的函数,因此生成的可执行文件较小。运行环境必须提供对应的库,动态函数库的改变不影响程序,动态库升级比较方便。

JNI 静态注册 和 动态注册

静态注册
实现流程
  1. 编写Java文件,定义native方法
  2. Java命令行编译得到.class .h 文件,将.h文件复制到 C 的项目中
  3. 定义 .c 文件,实现 .h 文件中的方法,添加 jni.h 头文件
  4. 编译 C项目 得到 .dll文件,回到Java中,加载 .dll 文件,实现JNI调用
具体实现

新建 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();
    }
}

运行效果:
在这里插入图片描述
JNI 静态注册这就实现了

动态注册
实现流程
  1. 编写Java文件,定义native 方法
  2. 在C项目中定义 .c 文件,对应实现Java中定义的native方法
  3. .c 文件中实现JNI_OnLoad 方法
  4. 编译C项目,得到.dll 文件,回到Java项目中加载 .dll文件,实现JNI调用
具体实现

首先,新建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 方法中帮我们匹配了方法调用;

C/C++ 访问 Java 中的变量

在上面的例子中,已经完成了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 <stdlib.h>
#include <string.h>
#include <windows.h>

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;

C/C++返回值给Java

前面的例子中,都是无返回值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方法

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 构造方法方法名必须传入 <init>
    jmethodID methodId = (*env) -> GetMethodID(env, clz, "<init>", "()V");
    // 3. 创建 要调用类的 对象
    jobject obj = (*env) -> NewObject(env, clz, methodId);
    // 4. 调用
    (*env) -> CallVoidMethod(env, obj, methodId);
};

调用构造方法,需要注意一点,方法名必须传入 <init>

调用非静态方法
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, "<init>", "()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语言中也存在三种引用:

  1. **全局引用:**调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,
    必须调用DeleteGlobalRef手动释放(*env)->DeleteGlobalRef(env,g_cls_string);
  2. **局部引用:**通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等),当函数执行完成后,函数内的局部引用生命周期也就结束了。
  3. ** 弱全局引用:**调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使
    用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用
    DeleteWeakGlobalRef手动释放(*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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值