美文网首页
JNI的探索

JNI的探索

作者: JasonChen8888 | 来源:发表于2019-12-10 14:41 被阅读0次

    JNI的概念

    • 定义
      JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植
    • 原理


      Jni原理图.png
      java平台.png
    • 开发工具
      1、vs2015
      2、eclipse(或者Android studio)
      3、java环境

    JNI的调用过程

    • 步骤:
      1.编写带有native声明的方法的java类
      2.编译生成class文件
      3.利用javah生成(.h)的头文件 命令:javah 类名, 注:不需要class后缀
      4.将(.h)头文件复制到vs下,创建(.cpp)或者(.c)文件实现(.h)头文件声明的方法
      5.实现完成后,编译成dll库
      6.将dll复制到java项目的根目录,调用System.loadLibrary("dll库名"); //注:不要dll后缀
      7.在代码里面调用native方法,访问native(.cpp 或者 .c)的代码

    • 栗子:
      1、java上的native定义

    //定义
    public native static String getStringFromCPP();
    //调用
    public static void main(String[] args) {
        System.out.println(getStringFromCPP());
    }
    

    2、利用javac或者编译器直接编译,生成class文件
    3、利用jdk下的javah 生成(.h)的头文件


    生成(.h)文件.png

    4、将生成的头文件放置到vs新建的项目,如下; 还需要将JDK目录下的include 目录下的jni.h 和 jni_md.h文件copy到项目
    因为生成的JniMain.h文件需要依赖到这两个文件,同时将JniMain.h中的 #include <jni.h> 改成 #include "jni.h"

    JNI的vs项目结构.png

    5、创建C++或者C文件实现JniMain的方法, 这边创建JniDemo.cpp, 引入头文件
    具体代码:

    #include "stdafx.h"
    #include "JniMain.h"
    #include <string.h>
    
    /*
    * Class:     JniMain
    * Method:    getStringFromCPP
    * Signature: ()Ljava/lang/String;
    */
    JNIEXPORT jstring JNICALL Java_JniMain_getStringFromCPP
    (JNIEnv *env, jclass jclaz) {
    return env->NewStringUTF("java static method call C++ back string");
    }
    

    6、打包成dll
    这边vs项目创建的是win32的项目,所以需要配置成dll
    在项目右键 ->属性


    配置类型改为Dll.png

    由于个人的环境是64位的,所以配置管理,需要修改为x64位


    配置管理.png
    配置管理器调整平台为x64.png

    生成dll


    生成dll.png

    7、将dll 复制到java项目工程的根目录,并加载dll库, 运营程序

    public class JniMain {
    
        //静态方法
        public native static String getStringFromCPP();
    
        static{
            System.loadLibrary("Jni");
        }
    
        public static void main(String[] args) {
            System.out.println(getStringFromCPP());
        }
    }
    

    结果:


    结果.png

    调用的分析

    JNI的数据类型

    • JNI基本数据类型:
    java C/C++
    boolean jboolean
    byte jbyte
    char jchar
    short jshort
    int jint
    long jlong
    float jfloat
    double jdouble
    • 引用类型:
    java C/C++
    String jstring
    Object jobject
    • 基本数据类型数组:
      //type[] jTypeArray;
      byte[] jByteArray;

    • 引用类型数组
      Object jobjectArray;

    JNI对应的java属性与方法签名

    在jni调用中,返回值和参数,以及静态字段和实例字段,有对应着相应的签名,如下表格:
    这些签名的时候在接下的实例讲解中会用到;
    简而言之,在jni中涉及到类型的使用(包括基本类和引用类型)


    Java属性与方法签名列表.png
    • 方法签名例子:
      方法1:
    public string addTail(String tail, int index)
    

    其对应的签名如下:

    (Ljava/util/String;I)Ljava/util/String;
    

    方法2:

    public int addValue(int index, String value,int[] arr)
    

    其对应的签名如下:

    (ILjava/util/String;[I)I
    
    • javap命令查看class文件中对应jni的签名
      命令:javap -s -p class文件的路径


      javap命令.png

    native修饰的静态方法

    • java代码:
    public native static String getStringFromCPP();
    
    • C++ 代码:
    /*
    * Class:     JniMain
    * Method:    getStringFromCPP
    * Signature: ()Ljava/lang/String;
    */
    JNIEXPORT jstring JNICALL Java_JniMain_getStringFromCPP
    (JNIEnv *env, jclass jclaz) {
        return env->NewStringUTF("java static method call C++ back string");
    }
    
    • 说明:
      java中定义native方法是静态的话,生成的接口方法的参数就是(JNIEnv *env, jclass jclaz)
      JNIEnv: 是jni接口调用的api指针
      jclass: 表示的就是native修饰的java静态方法所在的类

    native修饰的非静态方法

    • java代码:
    public native String getStringFromCPP2();
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    getStringFromCPP2
    * Signature: ()Ljava/lang/String;
    */
    JNIEXPORT jstring JNICALL Java_JniMain_getStringFromCPP2
    (JNIEnv *env, jobject job) {
        return env->NewStringUTF("java non-static method call C++ back string ");
    }
    
    • 说明:
      java中定义native方法是非静态的话,生成的接口方法的参数是(JNIEnv *env, jobject job)
      JNIEnv: 是jni接口调用的api指针
      jobject: 表示的就是native修饰的java非静态方法所在类的对象

    访问java类中的成员变量

    • java代码:
    public String key = "key";
    public native void accessField(); //该native方法用于调用c++的接口访问java变量
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    accessField
    * Signature: ()V
    */
    JNIEXPORT void JNICALL Java_JniMain_accessField
    (JNIEnv *env, jobject job) {
        //先获取对应的java类
        jclass jclaz = env->GetObjectClass(job);
    
        //第二个参数对应的是java的变量名,第三个是类型的签名
        jfieldID fid = env->GetFieldID(jclaz, "key", "Ljava/lang/String;");
    
        char text[100] = "Jni change string field value ";
    
        if (fid == NULL) { // 如果字段为 NULL ,直接退出,查找失败
            return;
        }
    
        // 获取字段对应的值
        jstring jstr = (jstring)env->GetObjectField(job, fid); 
        const char * str = env->GetStringUTFChars(jstr, NULL);
    
        strcat(text, str);
    
        jstring key = env->NewStringUTF(text);
        //修改key的值
        env->SetObjectField(job, fid, key);
    
    }
    
    • 说明:类似java的反射,步骤如下:
      1、获取 Java 对象的类
      2、获取对应字段的 id
      3、获取具体的字段值

    访问java类中的静态变量

    • java代码:
    public static int count = 9;
    public native void accessStaticField();  //该native方法用于调用c++的接口访问java静态变量
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    accessStaticField
    * Signature: ()V
    */
    JNIEXPORT void JNICALL Java_JniMain_accessStaticField
    (JNIEnv *env, jobject job) {
        //获取类名
        jclass jclaz = env->GetObjectClass(job);
    
        //获取静态字段  第一个参数是类,第二个参数对应的是java的变量名,第三个是类型的签名
        jfieldID fid = env->GetStaticFieldID(jclaz, "count", "I");
    
        if (fid == NULL) {
            return;
        }
    
        // 获取字段对应的值
        jint count = env->GetStaticIntField(jclaz, fid);
    
        count = 20;
        // 修改字段的值
        env->SetStaticIntField(jclaz, fid, count);
    }
    
    • 说明:静态字段的访问类似实例字段的访问,步骤相同

    jni中调用java某个对象的方法

    • java代码:
    public class Animal {
    
        protected String name;
        public static int num = 0;
        public Animal(String name) {
            this.name = name;
        }
        //jni访问的非静态方法
        public void setName(String name) {
            this.name = name;
        }
    
        public String getName() {
            return this.name;
        }
        public int getNum() {
            return num;
        }
    
        public static String getUID(String id) {
            return "10001"+id;
        }
    }
    //另一个类的native方法
    public native void callInstanceMethod(Animal animal);
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    callInstanceMethod
    * Signature: (LAnimal;)V
    */
    JNIEXPORT void JNICALL Java_JniMain_callInstanceMethod
    (JNIEnv *env, jobject instance, jobject animal) {
    
        // 获得具体的类
        jclass cls = env->GetObjectClass(animal);
        // 获得具体的方法 id
        jmethodID mid = env->GetMethodID(cls, "setName", "(Ljava/lang/String;)V");
    
        if (mid == NULL) {
            return;
        }
        //设置要穿的参数
        jstring name = env->NewStringUTF("baozi");
        //调用java的方法
        env->CallVoidMethod(animal, mid, name);
    }
    
    • 说明:
      与访问字段不同的是,GetFieldID 方法换成了 GetMethodID 方法,另外由 CallVoidMethod 函数来调用具体的方法,前面两个参数是获得的类和方法 id,最后的参数是具体调用方法的参数(签名)。方法签名 = (参数类型额签名) + 返回值类型的签名
      GetMethodID 方法的第一个参数就是具体的 Java 类型,第二个参数是该 Java 类的对应实例方法的名称,第三个参数就是该方法对应的返回类型和参数签名转换成 Native 对应的描述。
      对于不需要返回值的函数,调用 CallVoidMethod 即可,对于返回值为引用类型的,调用 CallObjectMethod 方法,对于返回基础类型的方法,则有各自对应的方法调用,比如:CallBooleanMethod、CallShortMethod、CallDoubleMethod 等等

    jni中调用java类的某个静态方法

    • java代码:
    public class Animal {
    
        protected String name;
        public static int num = 0;
        public Animal(String name) {
            this.name = name;
        }
        //jni访问的非静态方法
        public void setName(String name) {
            this.name = name;
        }
    
        public String getName() {
            return this.name;
        }
        public int getNum() {
            return num;
        }
        //jni访问的静态方法
        public static String getUID(String id) {
            return "10001"+id;
        }
    }
    //另一个类的native方法
    public native String callStaticMethod(Animal animal);
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    callStaticMethod
    * Signature: (LAnimal;)Ljava/lang/String;
    */
    JNIEXPORT jstring JNICALL Java_JniMain_callStaticMethod
    (JNIEnv *env, jobject instance, jobject animal) {
    
        //获取具体的类
        jclass jclz = env->FindClass("Animal");//参数也累的路径名
    
        // 获取具体的静态方法的 id
        jmethodID mid = env->GetStaticMethodID(jclz, "getUID", "(Ljava/lang/String;)Ljava/lang/String;");
        if (mid == NULL) {
            return env->NewStringUTF("method no found!");
        }
        jstring id = env->NewStringUTF("xxxxxx");
        jstring result = (jstring)env->CallStaticObjectMethod(jclz, mid, id);
        return result;
    }
    
    • 说明:
      与访问非静态不同的是,GetMethodID 方法换成了 GetStaticMethodID 方法,另外由 CallStaticObjectMethod 函数来调用具体的方法,前面两个参数是获得的类和方法 id,最后的参数是具体调用方法的参数(签名)。
      GetStaticMethodID 方法的第一个参数就是具体的 Java 类型,第二个参数是该 Java 类的对应实例方法的名称,第三个参数就是该方法对应的返回类型和参数签名转换成 Native 对应的描述。对于不需要返回值的函数,调用 CallStaticVoidMethod 即可,对于返回值为引用类型的,调用 CallStaticObjectMethod 方法,对于返回基础类型的方法,则有各自对应的方法调用,比如:CallStaticBooleanMethod、CallStaticShortMethod、CallStaticDoubleMethod 等等

    调用java类的构造方法

    • java代码:
    //访问构造方法
    public native Date acceessConstructor();
    
    • C++ 代码:
    /*
    * Class:     JniMain
    * Method:    acceessConstructor
    * Signature: (LAnimal;)Ljava/lang/Object;
    */
    JNIEXPORT jobject JNICALL Java_JniMain_acceessConstructor
    (JNIEnv *env, jobject job) {
        //通过类的路径来从JVM里面找到对应的类
        jclass jclz = env->FindClass("java/util/Date");
        //jmethodId  构造方法
        jmethodID jmid = env->GetMethodID(jclz, "<init>","()V");
    
        if (jmid == NULL) {
            return NULL;
        }
    
        // 调用newObject 实例化Date 对象,返回值是一个jobject
        jobject date_obj = env->NewObject(jclz, jmid);
    
        // 得到对应的对象方法,前提是,我们访问了相关对象的构造函数创建了这个对象
        jmethodID time_mid = env->GetMethodID(jclz, "getTime","()J");
        jlong time = env->CallLongMethod(date_obj, time_mid);
    
        printf("time: %lld \n", time);
        return date_obj;
    }
    
    • 说明
      调用java类的构造方法,等于在C++里面创建一个java对象,然后进行调用;同样也是采用GetMethodID的方法进行获取构造函数的id,然后由NewObject 进行对象的创建

    JNI数组的使用

    • java代码:
    //整型数据在C++中进行排序
    public native void giveArray(int[] inArray);
    public native int[][] initInt2DArray(int size);
    public native String[] initStringArray(int size);
    //调用
    int[] array = {3,9,2,50,6,13};
    jniMain.giveArray(array);
    for(int i=0; i< array.length; i++) {
        System.out.println(array[i]);
    }
    
    String[] strArr = jniMain.initStringArray(5);
    for (int i = 0; i < strArr.length; i++) {
        System.out.println("strArr["+i+"] = "+strArr[i]);
    }
    
    int[][] intArr = jniMain.initInt2DArray(4);
    for(int i =0; i < 4; i++) {
        for(int j = 0; j < 3; j ++) {
            System.out.println("arr["+i+"]["+j+"] = "+intArr[i][j]);
        }
    }
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    giveArray
    * Signature: ([I)V
    */
    JNIEXPORT void JNICALL Java_JniMain_giveArray
    (JNIEnv *env, jobject job, jintArray array) {
        //jintArray -> jint *
        jint *elemts = env->GetIntArrayElements(array, NULL);
        if (elemts == NULL)
        {
            return;
        }
        //数组长度
        int len = env->GetArrayLength(array);
        qsort(elemts, len, sizeof(jint), compare);
        //释放可能的内存
        //将JNI  修改的数据重新写回原来的内存
        env->ReleaseIntArrayElements(array, elemts, JNI_COMMIT);
    }
    
    /*
    * Class:     JniMain
    * Method:    initInt2DArray
    * Signature: (I)[[I
    */
    JNIEXPORT jobjectArray JNICALL Java_JniMain_initStringArray
    (JNIEnv *env, jobject job, jint size) {
        //创建jobjectArray对象
        jobjectArray result;
        jclass jclz;
        int i;
        jclz = env->FindClass("java/lang/String");
        if (jclz == NULL) {
            return NULL;
        }
        result = env->NewObjectArray(size,jclz, job);
        if (result == NULL) {
        return NULL;
        }
        //赋值
        for ( i = 0; i < size; i++)
        {
            char * c_str = (char *)malloc(256);
            memset(c_str, 0, 256);
            //将 int 转换成为 char
            sprintf(c_str, "hello num: %d\n",i);
            // C -> jstring
            jstring str = env->NewStringUTF(c_str);
            if (str == NULL) {
                return NULL;
            }
    
            // 将jstring 赋值给数组
            env->SetObjectArrayElement(result, i, str);
            free(c_str);
            c_str = NULL;
        }
    
        return result;
    }
    
    /*
    * Class:     JniMain
    * Method:    initInt2DArray
    * Signature: (I)[[I
    */
    JNIEXPORT jobjectArray JNICALL Java_JniMain_initInt2DArray
    (JNIEnv *env, jobject job, jint size) {
        //返回对象,是个二维数组
        jobjectArray ret;
        int i = 0;
        int j = 0;
        jclass intArrayClz = env->FindClass("[I");
        if (intArrayClz == NULL) {
           return NULL;
        }
    
        ret = env->NewObjectArray(size * 3, intArrayClz, NULL);
    
        jint tmp[3];//固定数组
        for ( i = 0; i < size; i++)
        {
            jintArray intArr = env->NewIntArray(3);
            for ( j = 0; j < 3; j++)
            {
                tmp[j] = i + j;
            }
    
            env->SetIntArrayRegion(intArr, 0, 3, tmp);
            //将一维数组的值复制到
            env->SetObjectArrayElement(ret, i, intArr);
            env->DeleteLocalRef(intArr);
        }
        return ret;
    }
    
    • 说明:
      jobjectArray 表示二维数组
      env->SetObjectArrayElement(ret, i, intArr); 二维数组的赋值
      env->NewObjectArray(size * 3, intArrayClz, NULL); 二维数组的创建

    处理中文字符串的乱码问题

    由于java的字符串编码,和C或者C++的字符串编码不一样,所以在java传中文到C/C++会出现乱码的现象


    字符编码切换.png

    解决方法,就是在C/C++直接调用java的String来处理

    • java代码:
    //定义
    public native String chineseChars2(String str);
    
    //调用
    System.out.println(jniMain.chineseChars2("宝宝22"));
    
    • C++代码:
    
    /*
    * Class:     JniMain
    * Method:    chineseChars2
    * Signature: (LAnimal;)Ljava/lang/Object;
    */
    JNIEXPORT jobject JNICALL Java_JniMain_chineseChars2
    (JNIEnv *env, jobject job, jstring in) {
    
         char * c_str = "马蓉与宝宝";
        //创建String的类
        jclass jclz = env->FindClass("java/lang/String");
        //获取构造函数的mid   这边使用的是java的String(byte[], string)的构造函数
        jmethodID mid = env->GetMethodID(jclz, "<init>", "([BLjava/lang/String;)V");
    
        //创建参数 jstring -> jbyteArray
        jbyteArray bytes = env->NewByteArray(strlen(c_str));
    
        // char * 赋值到byte数组中
        env->SetByteArrayRegion(bytes, 0, strlen(c_str), (jbyte*)c_str);
        // 设置编码
        jstring charsetName = env->NewStringUTF("GB2312");
    
        return env->NewObject(jclz, mid, bytes, charsetName);
    }
    

    JNI中 局部引用

    • java代码:
    public native void localRef();
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    localRef   局部引用
    * Signature: ()V
    */
    JNIEXPORT void JNICALL Java_JniMain_localRef
    (JNIEnv *env, jobject job) {
        int i;
        for ( i = 0; i < 5; i++)
        {
            jclass clz = env->FindClass("java/util/Date");
            jmethodID mid = env->GetMethodID(clz, "<init>", "()V");
            //创建对象
            jobject date_obj = env->NewObject(clz, mid);
            //使用这个引用
            jmethodID time_mid = env->GetMethodID(clz, "getTime", "()J");
            jlong time = env->CallLongMethod(date_obj, time_mid);
    
            printf("local reference time: %lld \n", time);
    
            //释放引用
            env->DeleteLocalRef(clz);
            env->DeleteLocalRef(date_obj);
        }
    }
    
    • 说明:
      局部引用通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止GC回收所引用的对象,不在本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef释放

    JNI中 全局引用

    • java代码:
    public native void createGlobalRef();
    public native String getglobalRef();
    public native void delGlobalRef();
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    createGlobalRef  创建全局引用
    * Signature: ()V
    */
    jstring global_str;
    JNIEXPORT void JNICALL Java_JniMain_createGlobalRef
    (JNIEnv *env, jobject job) {
        jstring str = env->NewStringUTF("JNI is intersting");
        global_str = (jstring)env->NewGlobalRef(str);
    }
    //全局引用
    //跨线程,跨方法使用
    // NewGlobalRef 是创建全局引用的唯一方法
    
    /*
    * Class:     JniMain
    * Method:    getglobalRef
    * Signature: ()Ljava/lang/String;
    */
    JNIEXPORT jstring JNICALL Java_JniMain_getglobalRef
    (JNIEnv *env, jobject job) {
        return global_str;
    }
    
    /*
    * Class:     JniMain
    * Method:    delGlobalRef  移除全局引用
    * Signature: ()V;
    */
    JNIEXPORT void JNICALL Java_JniMain_delGlobalRef
    (JNIEnv *env, jobject job) {
        env->DeleteGlobalRef(global_str);
    }
    
    • 说明
      全局引用,变量是定义在方法外,调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteGlobalRef手动释放env->DeleteGlobalRef(g_cls_string);

    JNI中 弱全局引用

    • java代码:
    public native String createWeakRef();
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    createWeakRef
    * Signature: ()Ljava/lang/String;
    */
    jstring g_weak_cls;
    JNIEXPORT jstring JNICALL Java_JniMain_createWeakRef
    (JNIEnv *env, jobject job) {
        jclass cls_string = env->FindClass("java/lang/String");
        g_weak_cls = (jstring)env->NewWeakGlobalRef(cls_string);
        g_weak_cls = env->NewStringUTF("Jni weak reference");
        printf("weak ref = %s \n ",g_weak_cls);
        return g_weak_cls;
    }
    
    • 说明:
      弱全局引用:调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef手动释放。env->DeleteWeakGlobalRef(g_cls_string)

    JNI中 异常处理

    • java代码:
    public native void exception();
    
    • C++代码:
    /*
    * Class:     JniMain
    * Method:    exception   异常处理
    * Signature: ()V;
    */
    JNIEXPORT void JNICALL Java_JniMain_exception
    (JNIEnv *env, jobject job) {
        jclass cls = env->GetObjectClass(job);
        jfieldID fid = env->GetFieldID(cls, "key11", "Ljava/lang/String;");
    
        //检查是否发送异常
        jthrowable ex = env->ExceptionOccurred();
        // 判断异常是否发送
        if (ex != NULL) {
            jclass newExc;
            //清空JNI 产生的异常
            env->ExceptionClear();
            //NullPointerException
            newExc = env->FindClass("java/lang/IllegalArgumentException");
        if (newExc == NULL)
        {
            printf("exception\n");
            return;
        }
        env->ThrowNew(newExc, "Throw exception from JNI: GetFieldID faild ");
        }
    }
    
    • 说明:
      native调用java中的方法,java中的方法抛出异常,我们在native中检测异常,检测到后抛出native中的异常,并清理异常。
      函数介绍:
      1> ExceptionCheck:检查是否发生了异常,若有异常返回JNI_TRUE,否则返回JNI_FALSE
      2> ExceptionOccurred:检查是否发生了异常,若用异常返回该异常的引用,否则返回NULL
      3> ExceptionDescribe:打印异常的堆栈信息
      4> ExceptionClear:清除异常堆栈信息
      5> ThrowNew:在当前线程触发一个异常,并自定义输出异常信息
      6> Throw:丢弃一个现有的异常对象,在当前线程触发一个新的异常
      7> FatalError:致命异常,用于输出一个异常信息,并终止当前VM实例(即退出程序)

    jni的静态注册和动态注册

    参考:https://blog.csdn.net/qq_20404903/article/details/80662316

    结语

    以上就是个人对jni的认知和总结,如有错误的地方,欢迎大家指出,该篇文件比较适合于对JNI的入门的同学
    项目代码:https://github.com/jasonkevin88/JniDemo

    相关文章

      网友评论

          本文标题:JNI的探索

          本文链接:https://www.haomeiwen.com/subject/bekggctx.html