Android jni 学习总结

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 
/* 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 
#include 
#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
  1. 运行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
  1. 编译生成的头文件 生成 JniFuncMain.h 文件
javah JniFuncMain
/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* 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 
#include 
#include 

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,"","(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
  1. 运行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格式 Java Native 占用内存大小(字节)
    B byte jbyte 1
    C char jchar 2
    D double jdouble 8
    F float jfloat 4
    I int jint 4
    S short jshort 2
    J long jlong 8
    Z boolean jboolean 1
    V void void
  2. 数组数据类型

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

Signature格式 Java Native
[B byte[] jbyteArray
[C char[] jcharArray
[D double[] jdoubleArray
[F float[] jfloatArray
[I int[] jintArray
[S short[] jshortArray
[J long[] jlongArray
[Z boolean[] jbooleanArray
  1. 复杂数据类型
    对象类型简称:L+classname+;
Signature格式 Java Native
Ljava/lang/String; String jstring
L+classname +; 所有对象 jobject
[L+classname +; Object[] jobjectArray
Ljava.lang.Class; Class jclass
Ljava.lang.Throwable; Throwable jthrowable
  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 

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 


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
  1. 运行 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层

请注意其中有一行

Looper.prepareMainLooper();

这就是为什么在子线程中使用 hander 要先调用 Looper.prepare(); 而主线程就不需要的原因
因为在主线程当中已经早已经调用过 prepareMainLooper 生成过 Looper

9. 其他事项

c和c++在jni编码风格

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

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

你可能感兴趣的:(Android jni 学习总结)