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

JNI 静态注册、动态注册

      • 什么是JNI
      • Java相关命令
      • Java 方法、变量的签名
      • JNI 数据类型
          • 基本类型
          • 引用类型
      • 静态库 和 动态库
      • JNI 静态注册 和 动态注册
        • 静态注册
          • 实现流程
          • 具体实现
        • 动态注册
          • 实现流程
          • 具体实现
      • C/C++ 访问 Java 中的变量
          • 访问普通变量
          • 访问静态变量
          • C/C++返回值给Java
      • C/C++ 调用Java方法
          • 调用构造方法
          • 调用非静态方法
          • 调用静态方法
      • 野指针问题

什么是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类型 签名
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 数据类型

基本类型
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 ;相比于静态库,在编译时并没有整合进目标代码,在程序执行到相关函数时才调用对应函数库的函数,因此生成的可执行文件较小。运行环境必须提供对应的库,动态函数库的改变不影响程序,动态库升级比较方便。

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项目
Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)_第1张图片
新建项目之后,将上一步生成的 .h 文件复制到 C 项目中,并且以同样的文件名新建一个 .c 文件,实现里面的函数
Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)_第2张图片
Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)_第3张图片
这两个参数代表的含义:
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)

添加完成之后编译项目Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)_第4张图片
编译完成后会在目录下生成 这么俩个文件, .dll 文件就是在Windows平台上生成的动态库,在Linux平台与之对应的就是 .so 库
Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)_第5张图片

回到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();
    }
}

运行效果:
Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)_第6张图片
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 文件:
Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)_第7张图片
回到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());
    }
}

运行结果:
Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)_第8张图片
动态注册相比于静态注册,省去了我们手动编译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 
#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;

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代码,运行结果:
Android进阶学习(10)-- JNI使用(Java 与 C/C++ 互相访问、调用)_第9张图片
这里我们会发现,打印的日志顺序反了,应该 下面两句 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 构造方法方法名必须传入 
    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语言中也存在三种引用:

  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;
}

你可能感兴趣的:(Android,进阶学习)