NDK系列:JNI基础

1 Java、JNI、C/C++中的数据类型之间的映射关系

JNI是接口,Java与C/C++交互会有一个数据类型的对应,而JNI为此提供了一套中间类型。

Java JNI 描述
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat signed 32 bits
double jdouble signed 64 bits
Class jclass class类对象
String jstring 字符串对象
Object jobject 任何Java对象
byte[] jbyteArray byte数组

2 JNI动态注册与静态注册

2.1 静态注册

步骤:

  • 编写Java类,比如StaticRegister.java;
package register.staticRegister;

public class StaticRegister {
    public static native String func();//注意native关键字
    public static void main(String[] args) {
        System.out.println(func());
    }
}
复制代码
  • 在.java源文件目录下,命令行输入“javac StaticRegister.java”生成StaticRegister.class文件;
  • 在StaticRegister.class所属包所在目录下,命令行执行“javah register.staticRegister.StaticRegister”(完整类名无后缀),在包所在目录生成register_staticRegister_StaticRegister.h头文件;

image.png

image.png

  • 如果是JDK 1.8或以上,以上步骤可简化为一步:在StaticRegister.java目录下,命令行执行 javac -h . StaticRegister.java,直接在当前目录下得到.class文件和.h文件;
  • 创建CLion项目并拷贝register_staticRegister_StaticRegister.h文件到项目目录;
  • 在CLion项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在C:\Program Files\Java\jdk1.8.0_144\include目录下,将这两个头文件拷贝到CLion项目目录;
  • 在register_staticRegister_StaticRegister.h中修改#include <jni.h>为#include "jni.h"
  • 我们其实可以看到,register_staticRegister_StaticRegister.h文件里面就是一个Java方面native方法的一个JNI声明,格式为JNIEXPORT 关键字一 jstring 返回值的JNI类型 JNICALL 关键字二 Java_register_staticRegister_StaticRegister_func Java_全类名_方法名 (JNIEnv *, jclass);如下;
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class register_staticRegister_StaticRegister */

#ifndef _Included_register_staticRegister_StaticRegister
#define _Included_register_staticRegister_StaticRegister
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     register_staticRegister_StaticRegister
 * Method:    func
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_register_staticRegister_StaticRegister_func
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif
复制代码
  • 编写头文件register_staticRegister_StaticRegister.h对应的register_staticRegister_StaticRegister.c源文件,拷贝并实现register_staticRegister_StaticRegister.h下的函数,如下:
#include "register_staticRegister_StaticRegister.h"

JNIEXPORT jstring JNICALL Java_register_staticRegister_StaticRegister_func
(JNIEnv *env, jclass jobj){
    return (*env)->NewStringUTF(env,"Hi Java, this is JNI");
};
复制代码
  • 编写CMakeLists.txt文件,这是库的配置文件。最重要的是最后两个add_library(),其余的都是自动生成的。add_library()声明库的名字、类型和包含的.c&.h文件。SHARED关键字表示创建的库是动态库.dll,STATIC关键字表示创建的库是静态库.a。*注意:库本身有动态库和静态库之分,Java native方法也有静态注册和动态注册之分,二者没有关系。*这里将动态库命名为StaticRegisterLib
cmake_minimum_required(VERSION 3.15)
project(JNI_C)

set(CMAKE_CXX_STANDARD 14)

add_library(JNI_C SHARED library.cpp library.h)
add_library(StaticRegisterLib SHARED register_staticRegister_StaticRegister.c register_staticRegister_StaticRegister.h)
复制代码
  • 此时CLion项目结构如下图,Build Project生成动态链接库,得到libStaticRegisterLib.dll

image.png

image.png

image.png

  • 在最开始的Java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:
package register.staticRegister;

public class StaticRegister {

    static {
        System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libStaticRegisterLib.dll");
    }

    public static native String func();
    public static void main(String[] args) {
        System.out.println(func());
    }
}
复制代码
  • 在Java侧运行,得到如下效果,Java成功调用了dll中的方法,静态注册完毕。

image.png

  • 上述过程,我们在JNI中使用Java_PACKAGENAME_CLASSNAME_METHODNAME与Java侧的方法进行匹配,这种方式我们称之为静态注册

2.2 动态注册

步骤:

  • 编写Java类,比如DynamicRegister.java,如下;
package register.dynamicRegister;

public class DynamicRegister {
    public static native String func1(String s);
    public static native int func2(int[] a);
    public static void main(String[] args) {
        System.out.println(func1("Hi JNI"));
        int[] a = {1,2,3};
        System.out.println("该数组有"+func2(a)+"个元素");
    }
}
复制代码
  • 在CLion项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在C:\Program Files\Java\jdk1.8.0_144\include目录下,将这两个头文件拷贝到CLion项目目录;
  • 新建CLion项目,新建C/C++源文件dynamicRegister.c。在该.c文件中,实现两个函数,这两个函数将是native方法在JNI的实现,如下:
#include "jni.h"

jstring f1(JNIEnv *env, jclass jobj){
    return (*env)->NewStringUTF(env,"Hi Java");
}
//注意JNI侧数组形参的写法以及如何求数组长度
jint f2(JNIEnv *env, jclass jobj, jintArray arr){
    int len = (*env)->GetArrayLength(env,arr);
    return len;
}
复制代码
  • 到目前,f1(),f2()与Java侧native方法func1(),func2()还没有任何关联,我们需要手动**管理关联**;
  • 首先,我们新建一个以JNINativeMethod结构体为元素的数组,如下:
static const JNINativeMethod mMethods[] = {
        {"func1","(Ljava/lang/String;)Ljava/lang/String;",(jstring *)f1},
        {"func2","([I)I",(jint *)f2},
};
复制代码
  • 以上数组中每一个元素,都是JNI侧实现方法与Java侧native方法的关联,前两个是Java侧native方法的描述,最后一个是JNI侧函数实现的描述,格式为:
{"Java侧的native方法名","方法的签名",函数指针}
复制代码
  • 我们需要实现jni.h中的JNI_OnLoad()方法,该方法的实现方法是一个模板,如下:
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved){
    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,"register/dynamicRegister/DynamicRegister");
    // 最后参数是需要注册的native方法的个数,如果小于0则注册失败。
    r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, 2);
    if(r != JNI_OK ){
        return -1;
    }
    return JNI_VERSION_1_4;
}
复制代码
  • 注意!第一:以上FindClass(env,"register/dynamicRegister/DynamicRegister")中的字符串是Java侧DynamicRegister类的全类名,注意此处的写法"/";第二:RegisterNatives(env, mainActivityCls, mMethods, 2)中的最后一个参数是需要动态注册的方法个数,手动添加注册或者删除注册都需要对应变化,当然可以直接把mMethod[]的长度传进去,一劳永逸,如下:
int cnt = sizeof(mMethods)/ sizeof(mMethods[0]);
r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, cnt);
复制代码
  • 在最开始的Java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:
package register.dynamicRegister;

public class DynamicRegister {
    static {
        System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libDynamicRegisterLib.dll");
    }
    public static native String func1(String s);
    public static native int func2(int[] a);
    public static void main(String[] args) {
        System.out.println(func1("Hi JNI"));
        int[] a = {1,2,3};
        System.out.println("该数组有"+func2(a)+"个元素");
    }
}
复制代码
  • Build Project生成动态链接库,得到libDynamicRegisterLib.dll
  • Java侧运行,效果如下:

image.png

  • 动态注册完毕。

3 system.load()与system.loadLibrary()

System.load() System.load()参数必须为库文件的绝对路径,可以是任意路径,例如: System.load("C:\Documents and Settings\TestJNI.dll"); //Windows System.load("/usr/lib/TestJNI.so"); //Linux

System.loadLibrary() System.loadLibrary()参数为库文件名,不包含库文件的扩展名。 System.loadLibrary("TestJNI"); //加载Windows下的TestJNI.dll本地库 System.loadLibrary("TestJNI"); //加载Linux下的libTestJNI.so本地库

注意:TestJNI.dll 或 libTestJNI.so 必须是在JVM属性java.library.path所指向的路径中。 loadLibary需要[配置当前项目的java.library.path路径]

3 JNI上下文与Java签名

3.1 JNI上下文环境

3.1.1 JNIEnv

JNIEnv类型实际上代表了Java环境,通过JNIEnv*指针,JNI函数可以对Java侧的代码进行操作。例如,创建Java类的对象,调用Java对象的方法,获取对象中的属性等。JNIEnv的指针会被传入到JNI侧的native方法的实现函数中,来对Java端的代码进行操作。例如:

jstring f1(JNIEnv *env, jclass jobj){
    return (*env)->NewStringUTF(env,"Hi Java");
}
复制代码

3.1.2 区分jobject与jclass

在JNI侧声明Java native方法的实现的时候,会有两个默认形参(除开native方法自己的传入参数),分别是JNIEnv指针,另外一个是jobject/jclass,这两个的区别在于:

  • jobject:如果Java侧的native方法是非静态的,那么传给JNI的第二个参数是类的对象,所有类的对象在JNI侧都是jobject类型。
  • jclass:如果Java侧的native方法是静态的,那么传给JNI的第二个参数是类的运行时类,所有运行时类在JNI侧都是jclass类型。

3.1.3 JNIEXPORT和JNICALL

JNIEXPORT和JNICALL是两个关键字,表明这个方法是JNI方法。

JNIEXPORT jint JNICALL Java_register_staticRegister_StaticRegister_func
(JNIEnv *env, jclass jobj,jintArray arr){
    int len = (*env)->GetArrayLength(env,arr);
    return len;
}
复制代码

3.2 签名

3.2.1 查看类中字段和方法的签名

javap命令:javap -s -p JniTest.class

image.png

3.2.2 Java字段的签名

很多时候,我们的签名除了根据命令行来定之外,其实还可以一句规律自己写出来,如下表:

数据类型 签名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
object L开头,然后以‘/’分隔包的完整类型,最后加上‘;’(分号)。
比如:
String签名是Ljava/lang/String;
Array [开头,后面加上元素类型的签名。
比如:
int[]的签名是[I;int[][]的签名是[[I;
object[]签名是[Ljava/lang/Object。

举例

//Java:
import java.util.Data;
public class Hello{
    public int property;
    public int function(int fu,Date date,int[] arr){
        return 0;
    }
    public native void test();
}
复制代码
//C++:
JNIEXPORT void JNICALL Java_Hello_test(JNIEnv *env,jobject jobj){
    jclass hello_clz = (*env)->GetObjectClass(env,jobj);
    jfieldID fieldID_prop = (*env)->GetFieldID(env,hello_claz,"property","I");
    jmethodID methodID_func = 
        (*env)->GetMethodID(env,hello_clz,"function","(ILjava/util/Date;[i)I");
    (*env)->CallIntMethod(env,jobj,methodID_func,0L,NULL,NULL);
}
复制代码

image.png

4 JNI调用Java

4.1 JNI侧获取Java类运行时类

区分FindClass()与GetObjectClass()

  • FindClass():JNI侧函数,相当于Java中的Class.forName(),通过Java侧的全类名获取到JNI可用的jclass变量
jclass jclz = 
    (*env)->FindClass(env,"packageName/ClassName");
复制代码
  • GetObjectClass():JNI侧函数,相当于Java侧的object.getClass(),通过从Java传到JNI侧的jobject对象类似于反射获取到该jobject对应的jclass变量
jclass jclz = (*env)->GetObjectClass(env,jobj);
复制代码

回顾:这里相当于涉及到了两种Java获取运行时类的方法,总共有四种,你还记得吗?

4.2 JNI调用Java

调用字段

Java侧的类的字段分为非静态字段与静态字段,JNI有对应的**GetXxx()/SetXxx()、GetStaticXxx()/SetStaticXxx()**方法,分为以下几个步骤:

  1. JNI注册:在JNI侧注册Java侧的native方法;
  2. 获取jclass:如果注册的是static的native方法,则在JNI实现中直接传入了jclass变量,可直接跳到下一个步骤;如果注册的是非static的native方法,则在JNI实现中传入了jobject变量,需要通过GetObjectClass()方法获取到jclass;
  3. 获取jfieldID:调用GetFieldID()/GetStaticFieldID();
  4. 获取到字段:调用GetXxxField()/GetStaticXxxField();
  5. 操作字段:调用SetXxxField()/SetStaticXxxField()。
package JNICallJava;

public class JNICallJava {
    static {
        System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libJNICallJavaLib.dll");
    }
    public int field1 = 1;
    public static int field2 = 8;
    public native void func1();//不传入field1,但是希望JNI能修改field1
    public static native void func2();//不传入field2,但是希望JNI能修改field2
    public static void main(String[] args) {
        JNICallJava jniCallJava = new JNICallJava();
        jniCallJava.func1();
        jniCallJava.func2();
        System.out.println(jniCallJava.field1);
        System.out.println(jniCallJava.field2);
    }
}
复制代码
//实现非静态native方法,调用操作非静态字段:
JNIEXPORT void JNICALL Java_JNICallJava_JNICallJava_func1
(JNIEnv *env, jobject jobj){
    jclass jclz = (*env)->GetObjectClass(env,jobj);
    jfieldID jfieldId = (*env)->GetFieldID(env,jclz,"field1","I");
    jint field1 = (*env)->GetIntField(env,jobj,jfieldId);
    (*env)->SetIntField(env,jobj,jfieldId,field1+10000);
}
复制代码
//实现静态native方法,调用操作静态字段:
JNIEXPORT void JNICALL Java_JNICallJava_JNICallJava_func2
(JNIEnv *env, jclass jclz){
    jfieldID jfieldId = (*env)->GetStaticFieldID(env,jclz,"field2","I");
    jint field2 = (*env)->GetStaticIntField(env,jclz,jfieldId);
    (*env)->SetStaticIntField(env,jclz,jfieldId,field2+10000);
}
复制代码

Java侧运行效果:

image.png

调用方法

注意:反射无法反射抽象方法,也没有必要反射抽象方法

在JNI看来,Java侧的方法分为:构造方法/静态方法/非静态方法。

Java侧代码

在进入JNI侧调用Java方法之前,先展示Java侧代码:

//一个与native方法无关的类,包含一个空参构造方法,一个非静态方法,一个静态方法。
package JNICallJava;

public class JNICallJavaMethod {
    public JNICallJavaMethod(){
        System.out.println("Java Side : I am constructor.");
    }
    public int func(int i){
        System.out.println("Java Side : func is called.");
        return i+100;
    }
    public static int staticFunc(int[] a){
        System.out.println("Java Side : staticFunc is called.");
        int sum = 0;
        for(int i=0;i<a.length;i++){
            sum += a[i];
        }
        return sum;
    }
}

复制代码
//一个包含native方法的类,还包含一个非静态方法。
package JNICallJava;

public class JNICallJavaMethodNative {
    static {
        System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libJNICallJavaMethodLib.dll");
    }
    //Java侧定义native方法
    public native int func1(int i);
    public String func2(){
        System.out.println("Java Side : I am func2 in the same class with the native func1.");
        return "Java Side : Hi JNI.";
    }
    public static void main(String[] args) throws InterruptedException {
        JNICallJavaMethodNative jniCallJavaMethodNative = new JNICallJavaMethodNative();
        int res = jniCallJavaMethodNative.func1(8);
        /**
         * 测试JNI侧的控制台打印与Java侧的控制台打印之间的顺序关系:
         * 结果发现,JNI侧的控制台打印一定是在Java之后,sleep也没用。
         */
        Thread.sleep(2000);
        System.out.println("Java Side : result of native func1 is "+res);
    }
}
复制代码

如上述代码所示,我们有两个类,第一个类JNICallJavaMethod与native方法与直接关系,里面提供了构造方法,非静态方法,静态方法供JNI调用。第二个类JNICallJavaMethodNative包含了native方法的声明,还包含了一个非静态方法供JNI调用;同时第二个类JNICallJavaMethodNative提供psvm作为整个程序的运行入口,程序开始运行后,在Java侧仅调用了native方法,得到了以下的结果:

image.png

JNI侧代码

#include <stdio.h>
#include "jni.h"

//JNIEXPORT jint JNICALL Java_JNICallJava_JNICallJavaMethodNative_func1
//        (JNIEnv*,jobject,jint);

JNIEXPORT jint JNICALL Java_JNICallJava_JNICallJavaMethodNative_func1
    (JNIEnv *env,jobject jobj,jint ji) {

    //todo:JNI侧创建一个int array
    jintArray jArr = (*env)->NewIntArray(env, 4);
    jint *arr = (*env)->GetIntArrayElements(env, jArr, NULL);
    arr[0] = 0;
    arr[1] = 10;
    arr[2] = 20;
    arr[3] = 30;
    (*env)->ReleaseIntArrayElements(env,jArr,arr,0);

    //todo:调用另一个类的构造函数
    jclass jclz1 = NULL;
    jclz1 = (*env)->FindClass(env, "JNICallJava/JNICallJavaMethod");
    if(jclz1 == NULL){
        printf("JNI Side : jclz is NULL.");
        return ji;
    }
    jmethodID jmethodId1 = NULL;
    jmethodId1 = (*env)->GetMethodID(env, jclz1, "<init>", "()V");
    if(jmethodId1 == NULL){
        printf("JNI Side : jmethodId1 is NULL.");
        return ji;
    }
    jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
    (*env)->CallVoidMethod(env, jobj1, jmethodId1);

    //todo:调用另一个类的非静态方法
    jmethodID jmethodId2 = NULL;
    jmethodId2 = (*env)->GetMethodID(env, jclz1, "func", "(I)I");
    if(jmethodId2 == NULL){
        return ji;
    }
    jobject jobj2 = (*env)->NewObject(env, jclz1, jmethodId2);
    jint i1 = (*env)->CallIntMethod(env, jobj2, jmethodId2, 5);
    printf("JNI Side : func returns %d.\n", i1);

    //todo:调用另一个类的静态方法
    jmethodID jmethodId3 = NULL;
    jmethodId3 = (*env)->GetStaticMethodID(env, jclz1, "staticFunc", "([I)I");
    if(jmethodId3 == NULL){
        printf("JNI Side : jmethodId3 is NULL.");
        return ji;
    }
    jint i2 = (*env)->CallStaticIntMethod(env, jclz1, jmethodId3, jArr);
    printf("JNI Side : staticFunc returns %d.\n", i2);

    //todo:调用与native方法同属一个类的方法
    jclass jclz0 = NULL;
    jclz0 = (*env)->GetObjectClass(env, jobj);
    if(jclz0 == NULL){
        printf("JNI Side : jclz0 is NULL.");
        return ji;
    }
    jmethodID jmethodId4 = NULL;
    jmethodId4 = (*env)->GetMethodID(env, jclz0,"func2","()Ljava/lang/String;");
    if(jmethodId4 == NULL){
        printf("JNI Side : jmethodId4 is NULL.");
        return ji;
    }
    //todo:接收Java方法返回的字符串,并在JNI侧打印
    jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
    char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env,jstr,0);
    printf("JNI Side : func2 returns %s\n",ptr_jstr);

    return ji+100;
}
复制代码

显然,这段JNI代码是native方法在JNI侧的实现,其中先后

  1. 创建了一个jintArray;
  2. 调用了Java侧JNICallJavaMethod类的构造方法;
  3. JNICallJavaMethod类的非静态方法;
  4. JNICallJavaMethod类的静态方法;
  5. JNICallJavaMethodNative类的非静态方法。

我们分别分析,以下代码就是上述代码的分别分析。

调用构造函数

步骤:

  1. 加载类,被调用方法所在类的运行时类,即jclass
  2. 获取方法ID,即jmethodID
  3. 创建一个类的实例,即jobject
  4. 调用方法。

注意:

  1. MethodName形参直接传""即可;
  2. 构造函数在Java侧没有返回值,连void都不是;但是,在JNI侧的方法签名中,返回值是void。比如,一个类的默认构造函数的签名是**"()V"**;
  3. 凡是JNI方法的*GetXxx()*过程,都必须进行异常处理,即使用前判断是否为NULL。

代码:

//todo:调用另一个类的构造函数
    jclass jclz1 = NULL;
    jclz1 = (*env)->FindClass(env, "JNICallJava/JNICallJavaMethod");
    if(jclz1 == NULL){
        printf("JNI Side : jclz is NULL.");
        return ji;
    }
    jmethodID jmethodId1 = NULL;
    jmethodId1 = (*env)->GetMethodID(env, jclz1, "<init>", "()V");
    if(jmethodId1 == NULL){
        printf("JNI Side : jmethodId1 is NULL.");
        return ji;
    }
    jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
    (*env)->CallVoidMethod(env, jobj1, jmethodId1);
复制代码

调用非静态方法

步骤:

  1. 加载类,被调用方法所在类的运行时类,即jclass
  2. 获取方法ID,即jmethodID
  3. 创建一个类的实例,即jobject
  4. 调用方法。

注意:

  1. 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;
  2. Java侧来说,一个类的对象可以调用多个方法;但是JNI侧的jobject是与jmethodID一一对应的。所以,即使JNI侧调用的不同方法属于同一个类,也需要创建不同的jobject,不能共用;从创建jobject的JNI函数可以看出来:
//jobject与jmethodID是一一对应的关系:
jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
复制代码

代码:

//todo:调用另一个类的非静态方法
    jmethodID jmethodId2 = NULL;
    jmethodId2 = (*env)->GetMethodID(env, jclz1, "func", "(I)I");
    if(jmethodId2 == NULL){
        return ji;
    }
    jobject jobj2 = (*env)->NewObject(env, jclz1, jmethodId2);
    jint i1 = (*env)->CallIntMethod(env, jobj2, jmethodId2, 5);
    printf("JNI Side : func returns %d.\n", i1);
复制代码

调用静态方法

步骤:

  1. 加载类,被调用方法所在类的运行时类,即jclass
  2. 获取方法ID,即jmethodID
  3. 调用方法。

注意:

  1. 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;
  2. 因为静态方法的调用不需要对象实例,所以调用Java静态方法时,不需要jobject。

代码:

//todo:调用另一个类的静态方法
    jmethodID jmethodId3 = NULL;
    jmethodId3 = (*env)->GetStaticMethodID(env, jclz1, "staticFunc", "([I)I");
    if(jmethodId3 == NULL){
        printf("JNI Side : jmethodId3 is NULL.");
        return ji;
    }
    jint i2 = (*env)->CallStaticIntMethod(env, jclz1, jmethodId3, jArr);
    printf("JNI Side : staticFunc returns %d.\n", i2);
复制代码

调用native方法所在类的方法

这里以非静态方法为例,因为Java侧方法与native方法在同一个类中,而JNI侧实现native方法时,会传入一个jclass/jobject,分别对应Java侧的native方法声明是static native/native。此时我们可以直接使用传入的jclass,或者利用**(*env)->GetObjectClass(env,jobj)**获取到运行时类。关键在于,调用哪个方法,首先需要加载该方法所在的类到JVM运行时环境中

代码:

//todo:调用与native方法同属一个类的方法
    jclass jclz0 = NULL;
    jclz0 = (*env)->GetObjectClass(env, jobj);
    if(jclz0 == NULL){
        printf("JNI Side : jclz0 is NULL.");
        return ji;
    }
    jmethodID jmethodId4 = NULL;
    jmethodId4 = (*env)->GetMethodID(env, jclz0,"func2","()Ljava/lang/String;");
    if(jmethodId4 == NULL){
        printf("JNI Side : jmethodId4 is NULL.");
        return ji;
    }
    //todo:接收Java方法返回的字符串,并在JNI侧打印
    jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
    char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env,jstr,0);
    printf("JNI Side : func2 returns %s\n",ptr_jstr);
复制代码

JNI调用Java方法答疑

JNI侧如何创建整形数组

步骤:

  1. 声明数组名字与数组长度,即jArr、4
  2. 获取数组元素类型(jint型)的指针,通过调用(*env)->GetIntArrayElements(env, jArr, NULL)
  3. 利用指针,为元素赋值;
  4. 释放指针资源,数组得以保留。

代码:

//todo:JNI侧创建一个int array
    jintArray jArr = (*env)->NewIntArray(env, 4);//步骤1
    jint *arr = (*env)->GetIntArrayElements(env, jArr, NULL);//步骤2
    arr[0] = 0;//步骤3
    arr[1] = 10;
    arr[2] = 20;
    arr[3] = 30;
    (*env)->ReleaseIntArrayElements(env,jArr,arr,0);//步骤4
复制代码
Java侧方法返回String,JNI调用时如何打印返回值?

步骤:

  1. 定义jstring变量,并用(jstring)强转jobject;
  2. 定义字符型指针,并用 (char *)强转;
  3. 打印。
//todo:接收Java方法返回的字符串,并在JNI侧打印
    jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
    char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env,jstr,0);
    printf("JNI Side : func2 returns %s\n",ptr_jstr);
复制代码
JNI侧与Java侧的控制台打印顺序

结论是:

JNI侧的控制台打印一定出现在Java侧程序运行结束之后。

我们可以调试看现象:

image.png

两遍I am constructor?

:在调用构造方法和非静态方法的两个调用过程中,都需要通过(*env)->NewObject(env, jclz, jmethodId)创建与jmethodID一一对应的jobject,所以调用了两次构造函数。

两遍func is called?

:待解答!

能否脱离native方法的实现来调用Java侧方法?

:可以,JNI是Java跨平台的实现机制,是Java与原生代码交互的机制。上述的过程我们一般都是在JNI侧的native方法实现中进行的,因为native方法的JNI实现中就有JNIEnv*指针,是获取JNIEnv*最容易的方式,并非唯一方式。如何获取JNIEnv*?待解答!

4.3 JNI处理从Java传来的字符串

Java与C字符串的区别

  • Java内部使用的是utf-16 16bit 的编码方式;
  • JNI里面使用的utf-8 unicode编码方式,英文是1个字节,中文3个字节;
  • C/C++ 使用ASCII编码,中文的编码方式GB2312编码,中文2个字节。

image.png

实战代码

//Java:
package JNICallJava;

public class GetSetJavaString {
    static {
        System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libGetSetJavaStringLib.dll");
    }
    public static native String func(String s);
    public static void main(String[] args) {
        String str = func("--Do you enjoy coding?");
        System.out.println(str);
    }
}
复制代码
//C:
#include "stdio.h"
#include "jni.h"
JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
        (JNIEnv *,jclass,jstring);//没有用专门的.h文件,此声明可写可不写。

JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
        (JNIEnv *env,jclass jclz,jstring jstr){
    const char *chr = NULL;//字符指针定义与初始化分开
    jboolean iscopy;//判断jstring转成char指针是否成功
    chr = (*env)->GetStringUTFChars(env,jstr,&iscopy);//&iscopy位置一般直接传入NULL就好
    if(chr == NULL){
        return NULL;//异常处理
    }
    char buf[128] = {0};//申请空间+初始化
    sprintf(buf,"%s\n--Yes, I do.",chr);//字符串拼接
    (*env)->ReleaseStringUTFChars(env,jstr,chr);//编程习惯,释放内存
    return (*env)->NewStringUTF(env,buf);
}
复制代码
//CMakeLists.txt
add_library(GetSetJavaStringLib SHARED  GetSetJavaString.c)
复制代码

运行结果

image.png

异常处理

上述代码实例中,GetStringUTFChars()方法将JNI的jstring变量转换成C语言能操作的char指针,这个过程可能失败,其实任何转换过程都可能失败,这些过程的目标变量的定义和初始化都需要分开进行,并通过判空进行异常处理

C语言字符串拼接

在C语言中,没有String,字符串都是字符指针。其拼接过程不像Java等语言那么简单,分为以下过程:

  1. malloc申请空间
  2. 初始化
  3. 拼接字符串
  4. 释放内存

灵活的静态注册

  • 此实战代码中,我们没有想一般的静态注册一样使用Java native产生的.h文件,而是直接在实现JNI方法之前写了一个JNI静态注册,这也是可行的,甚至这个提前的声明注册也是可以不写的。此时我们在CMakeLists.txt中的add_library()中值包含了该.c文件。核心在于add_library()中一定要包含native方法在JNI的实现函数,.h文件更多是Java命令生成的教你怎么写JNI实现的一个辅助,无关紧要。
  • JNI无视Java侧的访问控制权限,但会区别静态或非静态。

5 JNI引用

5.1 三种引用

只有当JNI返回值是jobject的引用,才是三种引用之一。

比如(*env)->GetMethodID()返回值就不是引用,是一个结构体。

局部引用

  • 绝大部分JNI方法返回的是局部引用;
  • 局部引用的作用域或者生命周期始于创建它的本地方法,终止于本地方法的返回;
  • 通常在局部引用不再使用时,可以显式使用**DeleteLocalRef()**方法来提前释放它所指向的对象,一边GC回收;
  • 局部引用时线程相关的,只能在创建他的线程里面使用,通过全局变量缓存并使用在其他线程是不合法的。

全局引用

调用NewGlobalRef()基于局部引用创建,会阻止GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteGlobalRef()手动释放。

弱全局引用

调用NewWeakGlobalRef()基于局部引用创建,不会阻止GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteWeakGlobalRef()手动释放。

5.2 野指针

上一次创建的东西在程序结束的被回收了,但是静态局部变量未释放,不为NULL。

作业1:写代码实现访问java 非静态和静态方法,返回值必须是object类型 作业2:写代码体会野指针异常

猜你喜欢

转载自juejin.im/post/7041939942636781576