JNI 使用介绍

389 阅读20分钟

1. 为啥使用 jni

jni 能够允许 Java 代码与 c/c++ 编写的应用程序和库进行交互,是将 Java 层(上层) 与 c/c++层(底层)的有机联系起来的桥梁

  • 运行速度快
  • 硬件控制,硬件代码通常使用 c 语言编写
  • 复用现有的优秀的 c/c++ 代码 (如 opencv ,ffmpeg)

2. 在 Java 中调用 c/c++ 库函数

  • 第一步: 编写 Java 代码 (native本地方法)
  • 第二步: 编译 Java 代码 (javac命令)
  • 第三步: 生成 c/c++ 语言头文件 (javah命令)
  • 第四步: 编写 c/c++ 代码(实现c/c++ 语言头文件的方法)
  • 第五步: 生成 c/c++ 共享库 (编译生成 .so)
  • 第六步: 运行 Java 代码 (java命令)
  1. 编写 Java 代码

    public class HelloJNI {
       static {
          System.loadLibrary("HelloJNI"); // Load native library at runtime
       }
     
       // Declare a native method sayHello() that receives nothing and returns void
       private native void sayHello();
     
       // Test Driver
       public static void main(String[] args) {
          new HelloJNI().sayHello();  // invoke the native method
       }
    }
  1. 编译 Java 代码 生成 HelloJNI.class 文件
    javac HelloJNI.java
  1. 编译生成的头文件 生成 HelloJNI.h 文件
    javah HelloJNI

生成的头文件

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

  1. 实现 HelloJNI.h 文件的方法 HelloJNI.cpp

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

    // Implementation of native method sayHello() of HelloJNI class
    JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
       printf("Hello World!\n");
       return;
    }
    
  1. 编译生成动态库
  • HelloJNI.dll (Windows)
  • libHelloJNI.jnilib(Mac)
  • libHelloJNI .so (Unixes)
gcc -shared -I /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/include/ -fPIC HelloJNI.cpp -o libHelloJNI.jnilib

6. 运行Java代码

java HelloJNI

3. c/c++ 库函数调用 Java 代码

  1. 编写 Java 代码

JniTest.java

public class JniTest {
    private int intField;

    public JniTest(int num) {
        intField = num;
        System.out.println("[java] 调用 JniTest 对象的构造方法: intField = " + intField);
    }

    public int callByNative(int num) {
        System.out.println("[Java] JniTest 对象的 callByNative(" + num + ") 调用");
        return num;
    }

    public void callTest() {
        System.out.println("[java] JniTest 对象的 callTest 方法调用 : intField = " + intField);
    }
}

JniFuncMain.java

public class JniFuncMain {
    private static int staticIntField = 300;

    static {
        System.loadLibrary("jnifunc");
    }

    public static native JniTest createJniObject();

    public static void main(String argv[]) {
        System.out.println("[java] createJniObject() 调用本地方法");
        JniTest jniObj = createJniObject();
        jniObj.callTest();
    }
}

  1. 编译 Java 代码 生成 JniTest.class JniFuncMain.class 文件
javac JniTest.java JniFuncMain.java

3. 编译生成的头文件 生成 JniFuncMain.h 文件

javah JniFuncMain
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniFuncMain */

#ifndef _Included_JniFuncMain
#define _Included_JniFuncMain
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JniFuncMain
 * Method:    createJniObject
 * Signature: ()LJniTest;
 */
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

  1. 实现 JniFuncMain.h 文件的方法 JniFuncMain.cpp

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

    JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject
            (JNIEnv * env, jclass clazz){
        jclass targetClass;
        jmethodID mid;
        jobject newObject;
        jstring helloStr;
        jfieldID fid;
        jint staticIntField;
        jint result;

        // 获取 JniFuncMain 类的 staticIntFiled 变量值
        fid = env->GetStaticFieldID(clazz,"staticIntField","I");
        staticIntField = env->GetStaticIntField(clazz,fid);
        printf("[CPP]获取 JniFuncMain 类的 staticIntFiled 值\n");
        printf("JniFuncMain.staticIntField = %d\n",staticIntField);

        // 查找生成对象的类
        targetClass = env->FindClass("JniTest");

        // 查找构造函数
        mid = env->GetMethodID(targetClass,"<init>","(I)V");

        // 生成 JniTest 对象(返回对象的引用)
        printf("[CPP] JniTest 对象生成\n");
        newObject = env->NewObject(targetClass,mid,100);

        // 调用对象的方法
        mid = env->GetMethodID(targetClass,"callByNative","(I)I");
        result = env->CallIntMethod(newObject,mid,200);

        // 设置 JniObject 对象的 intField 值
        fid = env->GetFieldID(targetClass,"intField","I");
        printf("[CPP] 设置 JniTest 对象的 intField 值为 200 \n");
        env->SetIntField(newObject,fid,result);

        // 返回对象引用
        return newObject;
    }
  1. 编译生成动态库 libjnifunc.jnilib
gcc -shared -I /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/include/ -fPIC JniFuncMain.cpp -o libjnifunc.jnilib

6. 运行Java代码

java JniFuncMain

4. jni 的基本概念

  1. Java 中使用 System.loadLibrary("...") 把动态库加载进去,不同系统动态库的命名不一样
  2. c/c++ 头文件分析
#define JNIEXPORT  __attribute__ ((visibility ("default")))
#define JNICALL

typedef _JNIEnv JNIEnv;
struct _JNIEnv {...}
  • JNIEXPORT , 用于定义与平台相关的宏,是用来做本地函数出现在编译的二进制(* .so文件)的动态表。它们可以(在这里更多信息 )设置为“隐藏”或“默认”。给编译器用的,一般不用管.

  • JNICALL 定义为空 编译器用,也不用管

  • JNIEnv 为 c 结构体,实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作,例如 创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被 JNI 传入到本地方法的实现函数中来对Java端的代码进行操作。

  • jobject 代表调用 native方法sayHello 方法的 Java 对象 (非静态方法会被传入)

  • jclass 代表调用调用该 native 方法 createJniObject 的类 (静态方法会被传入)

  • 方法名 java native方法对应 c/c++方法名为 Java_类名_方法名

    • 类名: 包含包名
    • 方法名: Java中的定义的方法名
  • jfieldID : java 类当中属性的的标识,获取或者赋值java 对象的属性 ,必须先获取 jfieldID

  • jmethodID : Java 类当中方法的标识,调用java方法 ,必须先获取 jmethodID

获取 jfieldID,jmethodID 必须提供在 Java 类当中的名字(属性名,方法名)和签名,签名是为了更好的找到 Java 当中的字段和方法,如果只有方法名的话不行,可能有方法重载

5. 各种数据类型和函数的签名格式

  1. 基本数据类型

    Signature格式JavaNative占用内存大小(字节)
    Bbytejbyte1
    Ccharjchar2
    Ddoublejdouble8
    Ffloatjfloat4
    Iintjint4
    Sshortjshort2
    Jlongjlong8
    Zbooleanjboolean1
    Vvoidvoid
  2. 数组数据类型

数组简称则是在前面添加: [

Signature格式JavaNative
[Bbyte[]jbyteArray
[Cchar[]jcharArray
[Ddouble[]jdoubleArray
[Ffloat[]jfloatArray
[Iint[]jintArray
[Sshort[]jshortArray
[Jlong[]jlongArray
[Zboolean[]jbooleanArray
  1. 复杂数据类型 对象类型简称:L+classname+;
Signature格式JavaNative
Ljava/lang/String;Stringjstring
L+classname +;所有对象jobject
[L+classname +;Object[]jobjectArray
Ljava.lang.Class;Classjclass
Ljava.lang.Throwable;Throwablejthrowable
  1. 函数签名

函数签名格式 : (参数签名)返回类型签名

Java函数对应签名
void foo()()V
float foo(int i)(I)F
long foo(int[] i)([I)J
double foo(Class c)(Ljava/lang/Class;)D
boolean foo(int[] i,String s)([ILjava/lang/String;)Z
String foo(int i)(I)Ljava/lang/String;

5.可使用javap命令获取签名

如果不确定Java 类中字段或者函数的签名格式,可以使用 javap 命令

javap -s -p 类名
    // -s表示输出Java签名
    // -p输出所有的类属性和方法成员
    // -c输出为指令模式

属性和方法的下面的 descriptor 就显示了该签名

6. 常用 jni 函数的使用

所有对 Java 代码的操作: 都是通过 JNIEnv * env 指针完成的

  1. java 属性操作
  • 首先要获取 jfieldID

根据是否静态变量调用不同的方法,但是参数都是类似的

参数:

  1. clazz: Java 类型
  2. name : Java 中的属性名
  3. sig : 属性签名
// 获取对象属性 jfieldID
    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig);

// 获取静态属性 jfieldID
    jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig);
  • 获取java对象属性的值

根据 java 对象属性的类型的不同需要调用不同的方法,但是参数都是类似的

参数:

  1. obj :要获取那个属性值的对象
  2. fieldID : java 类当中属性的的标识
    jobject GetObjectField(jobject obj, jfieldID fieldID);
    jboolean GetBooleanField(jobject obj, jfieldID fieldID);
    jbyte GetByteField(jobject obj, jfieldID fieldID);
    jchar GetCharField(jobject obj, jfieldID fieldID);
    jshort GetShortField(jobject obj, jfieldID fieldID);
    jint GetIntField(jobject obj, jfieldID fieldID);
    jlong GetLongField(jobject obj, jfieldID fieldID);
    jfloat GetFloatField(jobject obj, jfieldID fieldID);
    jdouble GetDoubleField(jobject obj, jfieldID fieldID);
  • 设置java对象属性的值

根据 java 对象属性的类型的不同需要调用不同的方法,但是参数都是类似的

参数:

  1. obj : 要设置属性值的对象
  2. fieldID : java 类当中属性的的标识
  3. value : 设置的值
    void SetObjectField(jobject obj, jfieldID fieldID, jobject value);
    void SetBooleanField(jobject obj, jfieldID fieldID, jboolean value);
    void SetByteField(jobject obj, jfieldID fieldID, jbyte value);
    void SetCharField(jobject obj, jfieldID fieldID, jchar value);
    void SetShortField(jobject obj, jfieldID fieldID, jshort value);
    void SetIntField(jobject obj, jfieldID fieldID, jint value);
    void SetLongField(jobject obj, jfieldID fieldID, jlong value);
    void SetFloatField(jobject obj, jfieldID fieldID, jfloat value);
    void SetDoubleField(jobject obj, jfieldID fieldID, jdouble value);
  • 获取java静态变量的值

根据 java 静态变量的类型的不同需要调用不同的方法,但是参数都是类似的

参数:

  1. clazz :要获取那个静态变量的类
  2. fieldID : java 类当中静态变量的的标识
    jobject GetStaticObjectField(jclass clazz, jfieldID fieldID);
    jboolean GetStaticBooleanField(jclass clazz, jfieldID fieldID);
    jbyte GetStaticByteField(jclass clazz, jfieldID fieldID);
    jchar GetStaticCharField(jclass clazz, jfieldID fieldID);
    jshort GetStaticShortField(jclass clazz, jfieldID fieldID);
    jint GetStaticIntField(jclass clazz, jfieldID fieldID);
    jlong GetStaticLongField(jclass clazz, jfieldID fieldID);
    jfloat GetStaticFloatField(jclass clazz, jfieldID fieldID);
    jdouble GetStaticDoubleField(jclass clazz, jfieldID fieldID);
  • 设置java静态变量的值

根据 java 静态变量的类型的不同需要调用不同的方法,但是参数都是类似的

参数:

  1. clazz: 要获取那个静态变量的类
  2. fieldID : java 类当中静态变量的的标识
  3. value : 设置的值
    void SetStaticObjectField(jclass clazz, jfieldID fieldID, jobject value);
    void SetStaticBooleanField(jclass clazz, jfieldID fieldID, jboolean value);
    void SetStaticByteField(jclass clazz, jfieldID fieldID, jbyte value);
    void SetStaticCharField(jclass clazz, jfieldID fieldID, jchar value);
    void SetStaticShortField(jclass clazz, jfieldID fieldID, jshort value);
    void SetStaticIntField(jclass clazz, jfieldID fieldID, jint value);
    void SetStaticLongField(jclass clazz, jfieldID fieldID, jlong value);
    void SetStaticFloatField(jclass clazz, jfieldID fieldID, jfloat value);
    void SetStaticDoubleField(jclass clazz, jfieldID fieldID, jdouble value);
  1. Java 方法调用
  • 首先获取方法标识 jmethodID

根据是否静态方法调用不同的方法,但是参数都是类似的

参数:

  1. clazz: 调用那个Java方法的类型
  2. name : Java 中的方法名
  3. sig : 方法签名
  jmethodID GetMethodID(jclass clazz, const char* name, const char* sig);
  jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig);
  • 调用普通方法 根据调用方法返回值不同,和传参格式不同有以下不同的方法

参数:

  1. obj: 调用那个的方法的对象
  2. methodID: 方法标识
  3. ...,arg: 传递的参数

jvalue 为类型联合体

typedef union jvalue {
    jboolean    z;
    jbyte       b;
    jchar       c;
    jshort      s;
    jint        i;
    jlong       j;
    jfloat      f;
    jdouble     d;
    jobject     l;
} jvalue;

    jobject     CallObjectMethod (jobject obj, jmethodID methodID, ...);
    jobject     CallObjectMethodV (jobject obj, jmethodID methodID, va_list args);
    jobject     CallObjectMethodA (jobject obj, jmethodID methodID, jvalue* args);
    
    jboolean    CallBooleanMethod  (jobject obj, jmethodID methodID, ...);
    jboolean    CallBooleanMethodV (jobject obj, jmethodID methodID, va_list args);
    jboolean    CallBooleanMethodA (jobject obj, jmethodID methodID, jvalue* args);
    
    jbyte       CallByteMethod  (jobject obj, jmethodID methodID, ...);
    jbyte       CallByteMethodV (jobject obj, jmethodID methodID, va_list args);
    jbyte       CallByteMethodA (jobject obj, jmethodID methodID, jvalue* args);
    
    jchar       CallCharMethod  (jobject obj, jmethodID methodID, ...);
    jchar       CallCharMethodV (jobject obj, jmethodID methodID, va_list args);
    jchar       CallCharMethodA (jobject obj, jmethodID methodID, jvalue* args);
    
    jshort      CallShortMethod  (jobject obj, jmethodID methodID, ...);
    jshort      CallShortMethodV (jobject obj, jmethodID methodID, va_list args);
    jshort      CallShortMethodA (jobject obj, jmethodID methodID, jvalue* args);
    
    jint        CallIntMethod  (jobject obj, jmethodID methodID, ...);
    jint        CallIntMethodV (jobject obj, jmethodID methodID, va_list args);
    jint        CallIntMethodA (jobject obj, jmethodID methodID,  jvalue*args);
    
    jlong       CallLongMethod  (jobject obj, jmethodID methodID, ...);
    jlong       CallLongMethodV (jobject obj, jmethodID methodID, va_list args);
    jlong       CallLongMethodA (jobject obj, jmethodID methodID, jvalue* args);
    
    jfloat      CallFloatMethod  (jobject obj, jmethodID methodID, ...);
    jfloat      CallFloatMethodV (jobject obj, jmethodID methodID, va_list args);
    jfloat      CallFloatMethodA (jobject obj, jmethodID methodID, jvalue* args);
    
    jdouble     CallDoubleMethod  (jobject obj, jmethodID methodID, ...);
    jdouble     CallDoubleMethodV (jobject obj, jmethodID methodID, va_list args);
    jdouble     CallDoubleMethodA (jobject obj, jmethodID methodID, jvalue* args);
    
    void        CallVoidMethod   (jobject obj, jmethodID methodID, ...);
    void        CallVoidMethodV  (jobject obj, jmethodID methodID, va_list args);
    void        CallVoidMethodA  (jobject obj, jmethodID methodID, jvalue* args);

  • 调用静态方法

根据调用方法返回值不同,和传参格式不同有以下不同的方法

参数:

  1. clazz: 调用那个静态方法的类型
  2. methodID: 方法标识
  3. ...,arg: 传递的参数

    jobject     CallStaticObjectMethod (jclass clazz, jmethodID methodID, ...);
    jobject     CallStaticObjectMethodV (jclass clazz, jmethodID methodID, va_list args);
    jobject     CallStaticObjectMethodA (jclass clazz, jmethodID methodID, jvalue* args);
    
    jboolean    CallStaticBooleanMethod  (jclass clazz, jmethodID methodID, ...);
    jboolean    CallStaticBooleanMethodV (jclass clazz, jmethodID methodID, va_list args);
    jboolean    CallStaticBooleanMethodA (jclass clazz, jmethodID methodID, jvalue* args);
    
    jbyte       CallStaticByteMethod  (jclass clazz, jmethodID methodID, ...);
    jbyte       CallStaticByteMethodV (jclass clazz, jmethodID methodID, va_list args);
    jbyte       CallStaticByteMethodA (jclass clazz, jmethodID methodID, jvalue* args);
    
    jchar       CallStaticCharMethod  (jclass clazz, jmethodID methodID, ...);
    jchar       CallStaticCharMethodV (jclass clazz, jmethodID methodID, va_list args);
    jchar       CallStaticCharMethodA (jclass clazz, jmethodID methodID, jvalue* args);
    
    jshort      CallStaticShortMethod  (jclass clazz, jmethodID methodID, ...);
    jshort      CallStaticShortMethodV (jclass clazz, jmethodID methodID, va_list args);
    jshort      CallStaticShortMethodA (jclass clazz, jmethodID methodID, jvalue* args);
    
    jint        CallStaticIntMethod  (jclass clazz, jmethodID methodID, ...);
    jint        CallStaticIntMethodV (jclass clazz, jmethodID methodID, va_list args);
    jint        CallStaticIntMethodA (jclass clazz, jmethodID methodID,  jvalue*args);
    
    jlong       CallStaticLongMethod  (jclass clazz, jmethodID methodID, ...);
    jlong       CallStaticLongMethodV (jclass clazz, jmethodID methodID, va_list args);
    jlong       CallStaticLongMethodA (jclass clazz, jmethodID methodID, jvalue* args);
 
    jfloat      CallStaticFloatMethod  (jclass clazz, jmethodID methodID, ...);
    jfloat      CallStaticFloatMethodV (jclass clazz, jmethodID methodID, va_list args);
    jfloat      CallStaticFloatMethodA (jclass clazz, jmethodID methodID, jvalue* args);

    jdouble     CallStaticDoubleMethod  (jclass clazz, jmethodID methodID, ...);
    jdouble     CallStaticDoubleMethodV (jclass clazz, jmethodID methodID, va_list args);
    jdouble     CallStaticDoubleMethodA (jclass clazz, jmethodID methodID, jvalue* args);

    void        CallStaticVoidMethod   (jclass clazz, jmethodID methodID, ...);
    void        CallStaticVoidMethodV  (jclass clazz, jmethodID methodID, va_list args);
    void        CallStaticVoidMethodA  (jclass clazz, jmethodID methodID, jvalue* args);


  1. Java 数组操作
  • 新建数组

根据数组中元素类型不同,有以下不同的方法

通用参数:

  1. length : 数组长度
// 对象数组
    jobjectArray NewObjectArray(jsize length, jclass elementClass,jobject initialElement);
 /*
 elementClass : 对象数据元素的类型
 initialElement : 初始化数组的对象,可以选择 NULL
 */   
    jbooleanArray NewBooleanArray(jsize length);

    jbyteArray NewByteArray(jsize length);

    jcharArray NewCharArray(jsize length);

    jshortArray NewShortArray(jsize length);

    jintArray NewIntArray(jsize length);

    jlongArray NewLongArray(jsize length);

    jfloatArray NewFloatArray(jsize length);

    jdoubleArray NewDoubleArray(jsize length);

  • 数组转化为指针

根据数组中元素类型不同,有以下不同的方法 参数 :

  1. array : 待转化的数组
  2. isCopy : 是否拷贝了一份 array数组数据,若返回的指针数组数据是拷贝则isCopy被置为JNI_TRUE,否则置为NULL或JNI_FALSE:

返回一个指向实际元素的指针, 或者分配一些内存来拷贝数据. 无论哪种方式, 返回的原始数据的指针在调用释放方法前是保证一直有效的(这意味着如果数据没有被拷贝, 这个对象数组将被限制在压缩堆数据时不能移动),必须自己释放每个你获取的数组, 同时如果Get方法失败的话,你的代码一定不能尝试释放一个NULL指针.

    jboolean* GetBooleanArrayElements(jbooleanArray array, jboolean* isCopy);
    
    jbyte* GetByteArrayElements(jbyteArray array, jboolean* isCopy);
    
    jchar* GetCharArrayElements(jcharArray array, jboolean* isCopy);
    
    jshort* GetShortArrayElements(jshortArray array, jboolean* isCopy);
    
    jint* GetIntArrayElements(jintArray array, jboolean* isCopy);
    
    jlong* GetLongArrayElements(jlongArray array, jboolean* isCopy);
    
    jfloat* GetFloatArrayElements(jfloatArray array, jboolean* isCopy);
    
    jdouble* GetDoubleArrayElements(jdoubleArray array, jboolean* isCopy);
  • 释放数组内存

根据数组中元素类型不同,有以下不同的方法

参数 :

  1. array : 待释放的数组
  2. elems : 之前调用 GetXXXArrayElements 返回的指针
  3. mode: 释放模式
    • 0 将内容复制回来并释放原生数组(对Java数组进行更新,并释放C/C++的数组)
    • JNI_COMMIT 将内容复制回来但不释放原生数组,一般用于周期性更新数组(对Java数组进行更新但不释放C/C++数组)
    • JNI_ABORT 释放原生数组但不将内容复制回来O (对Java数组不进行更新,并释放C/C++数组)
    void ReleaseBooleanArrayElements(jbooleanArray array, jboolean* elems,jint mode);

    void ReleaseByteArrayElements(jbyteArray array, jbyte* elems,jint mode);

    void ReleaseCharArrayElements(jcharArray array, jchar* elems,jint mode);

    void ReleaseShortArrayElements(jshortArray array, jshort* elems,jint mode);

    void ReleaseIntArrayElements(jintArray array, jint* elems,jint mode);

    void ReleaseLongArrayElements(jlongArray array, jlong* elems,jint mode);

    void ReleaseFloatArrayElements(jfloatArray array, jfloat* elems, jint mode);

    void ReleaseDoubleArrayElements(jdoubleArray array, jdouble* elems,jint mode);

  • 拷贝数组内容到 buf 指针

根据数组中元素类型不同,有以下不同的方法

参数:

  1. array :待拷贝的数组
  2. start: 拷贝数组的起始位置
  3. len: 拷贝数组的个数
  4. buf: 目标指针
    void GetBooleanArrayRegion(jbooleanArray array, jsize start, jsize len,
        jboolean* buf)
    void GetByteArrayRegion(jbyteArray array, jsize start, jsize len,
        jbyte* buf)
    void GetCharArrayRegion(jcharArray array, jsize start, jsize len,
        jchar* buf)
    void GetShortArrayRegion(jshortArray array, jsize start, jsize len,
        jshort* buf)
    void GetIntArrayRegion(jintArray array, jsize start, jsize len,
        jint* buf)
    void GetLongArrayRegion(jlongArray array, jsize start, jsize len,
        jlong* buf)
    void GetFloatArrayRegion(jfloatArray array, jsize start, jsize len,
        jfloat* buf)
    void GetDoubleArrayRegion(jdoubleArray array, jsize start, jsize len,
        jdouble* buf)
  • 拷贝buf指针内容到数组

根据数组中元素类型不同,有以下不同的方法

参数:

  1. array :要被赋值的目标数组
  2. start: 被赋值数组的起始位置
  3. len: 拷贝的个数
  4. buf: 被拷贝的数据
    void SetBooleanArrayRegion(jbooleanArray array, jsize start, jsize len,
        const jboolean* buf)
    void SetByteArrayRegion(jbyteArray array, jsize start, jsize len,
        const jbyte* buf)
    void SetCharArrayRegion(jcharArray array, jsize start, jsize len,
        const jchar* buf)
    void SetShortArrayRegion(jshortArray array, jsize start, jsize len,
        const jshort* buf)
    void SetIntArrayRegion(jintArray array, jsize start, jsize len,
        const jint* buf)
    void SetLongArrayRegion(jlongArray array, jsize start, jsize len,
        const jlong* buf)
    void SetFloatArrayRegion(jfloatArray array, jsize start, jsize len,
        const jfloat* buf)
    void SetDoubleArrayRegion(jdoubleArray array, jsize start, jsize len,
        const jdouble* buf)
  • 数组其他函数
// 获取数组长度
    jsize GetArrayLength(jarray array);
    /*
    array:待获取长度的数组
    */
// 获取对象数组相应索引位置的元素    
    jobject GetObjectArrayElement(jobjectArray array, jsize index);
  /*
  array: 对象数组
  index: 索引位置
  */  
// 设置对象数组相应索引位置的元素
    void SetObjectArrayElement(jobjectArray array, jsize index, jobject value)
  /*
    array: 对象数组  
    index: 索引位置
    value: 新的元素对象
  */  

    
  1. Java 字符串操作

Java 编程语言使用的是 UTF-16。为方便起见,JNI 还提供了使用修改后的 UTF-8 的方法。修改后的编码对 C 代码非常有用,因为它将 \u0000 编码为 0xc0 0x80,而不是 0x00。这样做的好处是,您可以依靠以零终止的 C 样式字符串,非常适合与标准 libc 字符串函数配合使用。但缺点是,您无法将任意 UTF-8 数据传递给 JNI 并期望它能够正常工作

// jchar (字符指针) 转化为 jstring (字符串) 
    jstring NewString(const jchar* unicodeChars, jsize len);
    /*
        1. unicodeChars : 要转化为 jstring 的 jchar 指针
        2. len : jchar指针 的字符个数
    */
// jstring (字符串) 转化为 jchar 字符指针
    const jchar* GetStringChars(jstring string, jboolean* isCopy);
    /*
    jstring : 要转化为 char 指针的 jstring
    isCopy :  是否拷贝了一份字符串资源
    */
// 释放字符串资源 ,当 jstring 转为jchar 指针时会拷贝一份字符串
    void ReleaseStringChars(jstring string, const jchar* chars);
    /*
        1. string : 待释放的 jstring
        2. chars : 调用 GetStringChars 生成的 jchar 指针
    */

// 获取 jstring 字符串长度
    jsize GetStringLength(jstring string);
   
  // 通过 c 语言字符串生成一个 jstring 对象

    jstring NewStringUTF(const char* bytes);
    
//  Java 中的 jstring 对象的转化为 c/c++中在字符指针处理
    const char* GetStringUTFChars(jstring string, jboolean* isCopy);
    /*
    jstring : 要转化为 char 指针的 jstring
    isCopy :  是否拷贝了一份字符串资源
    */
// 释放字符串资源 ,当 jstring 转为 char 指针时会拷贝一份字符串
    void ReleaseStringUTFChars(jstring string, const char* utf);
    /*
        1. string : 待释放的 jstring
        2. chars : 调用 GetStringUTFChars 生成的 char 指针
    */
 // 获取 jstring 字符串长度 
     jsize GetStringUTFLength(jstring string);

// 拷贝部分字符到 buf 当中
    void GetStringRegion(jstring str, jsize start, jsize len, jchar* buf)
    /*
        1. str : 待拷贝的字符串
        2. start : 待拷贝的起始位置
        3. len : 要拷贝的长度
        4. buf : 目标字符指针
    */
// 拷贝部分utf 字符到 buf 当中
    void GetStringUTFRegion(jstring str, jsize start, jsize len, char* buf)
     /*
        1. str : 待拷贝的字符串
        2. start : 待拷贝的起始位置
        3. len : 要拷贝的长度
        4. buf : 目标字符指针
    */
  1. Java 类型,对象操作
/* 通过类名查找到类型,类名要完整的,包含包名 且包名之间分割 . 要换成 / 
    例如 包名为 net.incivility.jnitest 类型是 JniTest net/incivility/jnitest/JniTest */
    jclass FindClass(const char* name)
// 通过对象obj 返回该对象的类型 jclass
    jclass GetObjectClass(jobject obj);
// 判断对象 obj 是否该类型 clazz 的对象
    jboolean IsInstanceOf(jobject obj, jclass clazz)
    
 // 判断两个对象引用 ref1,ref2是否指向相同的对象   
    jboolean IsSameObject(jobject ref1, jobject ref2)
      
 // clazz2 是否是 clazz1 相同的类型或者是其子类 (clazz2类型的对象是否能给 clazz1类型的对象的值赋值)
    jboolean IsAssignableFrom(jclass clazz1, jclass clazz2)
//  获取 clazz 的父类,如果没有父类返回 NULL
    jclass GetSuperclass(jclass clazz)
    
 // 不调用构造函数创建对象,对象属性会只会被默认初始化为空 
    jobject AllocObject(jclass clazz)
    
//   调用构造函数创建对象

    jobject NewObject(jclass clazz, jmethodID methodID, ...)

    jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args)

    jobject NewObjectA(jclass clazz, jmethodID methodID, jvalue* args)
    
    /*
        1. clazz: 要创建对象的类型
        2. methodID: 构造函数方法 jmethodID 标识
        3. ...,args: 构造函数参数
    */
    
  1. 引用操作
 //  引用分类
typedef enum jobjectRefType {
    JNIInvalidRefType = 0, // 该 obj 是个无效的引用,说明对象已被回收
    JNILocalRefType = 1,  // 该 obj 是个局部引用 ,普通函数里面生成的对象引用,函数调用完成返回就失效了
    JNIGlobalRefType = 2,  // 该 obj 是个全局引用,除非主动调用删除引用方法,一直有效,不会被垃圾回收器回收
    JNIWeakGlobalRefType = 3 
    /* 
    该 obj是个全局的弱引用,除了主动调用删除引用方法外,可能会被垃圾回收器回收,使用该引用时应检查引用的有效性
    */
} jobjectRefType;

// 创建局部引用
    jobject NewLocalRef(jobject ref);
// 释放局部引用
    void DeleteLocalRef(jobject localRef);
//  创建全局引用
    jobject NewGlobalRef(jobject obj)
//   释放全局引用
    void DeleteGlobalRef(jobject globalRef)
    
//   创建全局弱引用  
    jweak NewWeakGlobalRef(jobject obj)
//   释放全局弱引用
    void DeleteWeakGlobalRef(jweak obj)
   

使用示例

因为通过类名加载类比较耗时,而以下函数又经常被调用,我们就可以定义一个全局引用保存 jclass 引用,即使函数调用完成,全局对象也不会被回收

#include "RefTestMain.h"

static jclass globalTargetClass =0;

JNIEXPORT jni JNICALL Java_RefTestMain_getMember(JNIEnv *env,jclass clazz)
{
    jfieldID fid;
    jint intField;
    jclass targetClass;
    
    if(globalTargetClass0)
    {
        targetClass=env->FindClass("RefTest");
        globalTargetClass=(jclass)env->newGlobalRef(targetClass);
    }
    fid=env->GetStaticFieldID(globalTargetClass,"intField","I");
    
    return intField
}

7. 加载本地库时,注册JNI本地函数

在Java代码中,System.loadLibrary()方法时,Java虚拟机会加载其参数指定的共享库,然后,Java虚拟机检索共享库中的函数符号

  1. 检查JNI_OnLoad()函数是否被实现,若共享库中含有相关函数,则JNI_OnLoad()函数就会被自动调用,我们可以使用 jni 提供的函数进行注册 native 方法

  2. 若库中的JNI_OnLoad()函数未被实现,则Java虚拟机会自动将本地方法与库内的JNI本地函数符号进行比较匹配。

我们现在的代码中都没有使用到 JNI_OnLoad() 函数,都是使用符合进行匹配找到 native 代码中的函数,使用固定格式,如

    JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
       printf("Hello World!\n");
       return;
    }

编写 HelloJNI.c

#include <jni.h>

void sayHello(JNIEnv * env, jobject obj);

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM * vm,void *reserved)
{
    JNIEnv *env=NULL;
    JNINativeMethod nm[1];
    jclass cls;
    jint result=-1;
    if ((*vm)->GetEnv(vm,(void **)&env,JNI_VERSION_1_8)!=JNI_OK)
    {
        printf("Error");
        return JNI_ERR;
    }

    cls=(*env)->FindClass(env,"HelloJNI");

    nm[0].name="sayHello";
    nm[0].signature="()V";
    nm[0].fnPtr=(void *)sayHello;
    (*env)->RegisterNatives(env,cls,nm,1);

    return JNI_VERSION_1_8;

}

void sayHello(JNIEnv * env, jobject obj)
{
    printf("JNI_OnLoad hello world!\n");
}


编译动态库

gcc -shared -I /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/include/ -fPIC HelloJNI.c -o libHelloJNI.jnilib

把新生成的 libHelloJNI.jnilib 替换旧的 运行 java HelloJNI

我们没有按照 jni 的固定格式一样能让 Java 找到我们的 sayHello 方法

    // 通过 JavaVM 指针获取JNIEnv * env

    jint GetEnv(JavaVM * vm,void ** env,jint version)
    /*
        1. vm : Java 虚拟机指针 JNI_OnLoad 方法中自带 参数
        2. env : Java 环境指针的指针
        3. version : jni 版本号
    */
    typedef struct {
        char *name; //Java类中的方法名
        char *signature;// 方法的签名
        void *fnPtr; // c/c++代码中的函数指针
    } JNINativeMethod;

    // 将Java类中的本地方法与JNI本地函数映射在一起
    jarray RegusterNatives(JNIEnv *env,jclass clazz,const JNINativeMethod *methods,jint nMethods);
    /*
        1. env : Java 环境指针
        2. clazz : 注册native 方法的类
        3. methods :  JNINativeMethod 结构体指针,里面有方法的映射信息
        4.nMethods : JNINativeMethod长度,即注册方法的个数
    */

android 框架中几乎都是使用 RegusterNatives 把 Java 方法和 native 方法进行绑定的

8. 在c/c++程序中运行Java类

JNI 提供了一套 API,允许本地代码在自身内存区域内加载Java虚拟机

  1. JNITest1.java

    import java.util.Arrays;

    public class JNITest1 {
    
        public static void main(String argv[])
        {
            System.out.println(Arrays.toString(argv));
        }
    }
  1. 编译 java 文件 生成 class
 javac JNITest1.java
  1. main.c
#include <jni.h>


int main(int argc, char** argv) {

    JNIEnv * env;
    
    JavaVM *vm;
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];
    
    jint res;
    jclass cls;
    jmethodID mid;
    jstring jstr;
    jclass stringClass;
    
    jobjectArray args;
    
    options[0].optionString="-Djava.class.path=.";
    vm_args.version=0x00010002;
    vm_args.options=options;
    vm_args.nOptions=1;
    vm_args.ignoreUnrecognized=JNI_TRUE;
    res=JNI_CreateJavaVM(&vm,(void **)&env,&vm_args);
    
    cls =(*env)->FindClass(env,"JNITest1");
    
    mid=(*env)->GetStaticMethodID(env,cls,"main","([Ljava/lang/String;)V");
    
    jstr=(*env)->NewStringUTF(env,"Hello Invocation API!!");
    
    stringClass=(*env)->FindClass(env,"java/lang/String");
    args=(*env)->NewObjectArray(env,1,stringClass,jstr);
    (*env)->CallStaticVoidMethod(env,cls,mid,args);
    
    (*vm)->DestroyJavaVM(vm);
     return (0);
}

  • JavaVM 代表 java 虚拟机
  • JNIEnv 代表 Java 环境 ,之前使用的各个操作 Java 代码的函数都是通过该指针使用的
  • JavaVMInitArgs 表示可以用来初始化 JVM 的各种 JVM 参数
  • JavaVMOption 具有用于 JVM 的各种选项设置
// 创建 Java 虚拟机
jint JNI_CreateJavaVM(JavaVM**, JNIEnv**, void*);

typedef struct JavaVMInitArgs {
    jint version; //jni版本
    jint nOptions;  // JavaVMOption结构体数组元素的个数
    JavaVMOption *options; // JavaVMOption结构体地址
    jboolean ignoreUnrecognized;//Java虚拟机遇到错误是否继续执行
} JavaVMInitArgs;

typedef struct JavaVMOption {
    char *optionString;
    void *extraInfo;
} JavaVMOption;

  1. 编译 main.c 文件,生成可执行文件 main
gcc -g -I/Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/include/ -L/Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/server/ -std=c11 main.c -ljvm -o main

6. 运行 main

直接从 c 代码中生成虚拟机运行 java 代码 System.out.println(Arrays.toString(argv)); 并把argv 参数数据传递到 Java 层

Android app 开发中我们没有看到 Java 当中的 main 方法,其实只是被Android框架隐藏了 (Android 的 main 方法在 android.app.ActivityThread 当中) 其实也是利用 Java Invocation API 运行 Java 类 把代码运行 从 c/c++ 层转移到Java层

9. 其他事项

c 和 c++ 在jni编码风格

在C中:

使用JNIEnv* env要这样      (*env)->方法名(env,参数列表)
使用JavaVM* vm要这样       (*vm)->方法名(vm,参数列表)

在C++中:
使用JNIEnv* env要这样      env->方法名(参数列表)
使用JavaVM* vm要这样       vm->方法名(参数列表)