JNI 基础用法相关总结

JNI

Sun JNI

JNI oracle 详细文档

  • JNI overview

    • 为什么需要 jni?
      • 有了标准以后,native 库可移植。
        • 统一 java 和 native 的互操作接口,使得这个接口不会受具体的 JVM 实现的影响。早期有一些 JVM 规定了私有的与 native 的交互,JVM 之间的 native 操作不兼容。
      • 一些时间要求严格的操作,使用低层次 native 代码进行,牺牲高级语言的方便,提高性能。
  • JNI 知识脉络

    • 与 java 的交互
      • java 调用 native
        • jni 方法 load
        • 数据类型 / jni 方法格式
      • native 调用 java
        • jniEnv 提供的 API
          • 反射 : 真的反射 + (类 / 对象/string/array + 方法 / 属性)
          • 锁,JVM 信息
  • JNI design

    • JNI 暴露给 native 的是一个 Pointer.Java 调用 native 的方法时,这个 Pointer 作为一个参数(即 JNIEnv,提供了很多操作 JVM 的方法,这些方法叫作 JNI 方法)

      • Pointer->Pointer->Pointer 是一个指针数组,每个指针指向一个方法。
    • 编译/ 链接 /加载

      • java 代码使用System.loadLibrary来加载 native 代码到内存中。JVM 内部为每一个classLoader维护一个已加载的 native library list.
    • JNI 方法名称解析

      • Java_切割的全称类名_切割的方法名[__如果方法重载,切割的方法参数签名]
      • java 方法和 native 方法同名不算重载
      • Unicode 字符以及一些特别的符号(比如_ / ; / [等)会进行相应的转义,转义成_0/_1/...,因为方法名以及类名等不会以数字为开头(所以 JNI 方法名的_数字不会有多重含义)。
    • native 方法参数

      • JNIEnv*是一个 JVM 指针,提供 JVM 对 native 的各种功能:access 对象,读取 native 调用 JNI 产生的 exception 等。
      • 对于静态 native 方法:f(JNIEnv*,jobject,args...),第二个参数为调用的 class
      • 对于非静态方法:f(JNIEnv*,jobject,args...),第二个参数为调用的对象。
    • native 引用 JVM 对象

      • 基本类型数据是直接 copy value 的

      • 其余类型传递引用到 native。所以

        1. 对 JVM 来说:JVM 必须对传入 native 的对象引用(reference)做额外的计数,才能保证这些对象不被 gc 清除。
        2. 对 native 代码来说:native 代码必须在不需要引用后,主动通知 JVM。
      • 对于 native 代码来说,对象的 reference 有两种:globallocal refs

        • global reference
          • 由 native 代码保存的 JVM 对象引用。
          • 由 native 代码调用globalRef = (*env)->NewGlobalRef(env, localRef)申请,调用(*env)->DeleteGlobalRef(env, globalRef)声明不再引用。
        • local reference 。 没有主动调用的类型
          • 存在周期:从 native 代码开始到 native 代码返回。JVM 在调用 native 方法时主动维护一个 local reference table,保存所有 native 代码引用的 local reference 防止其对象被 gc 掉,在 native 方法结束后将这些 reference 清空,允许 gc 清除。native 代码可以主动调用(*env)->DeleteLocalRef(env, localRef)来允许 JVM 回收这个对象。
          • 包括范围
            • 传入 native 代码的参数;
            • native 代码返回的参数。
            • 包不包括 native 代码请求 JVM 生成的对象呢?
        • 被 native 代码持有的 local reference 和 global reference 都不会被 gc 掉。
      • native 代码获取对象的 property 和调用对象的 method

        • 步骤
          1. jmethodID mid = env->GetMethodID(cls, “f”, “(ILjava/lang/String;)D”);
          2. jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
        • 获取到methodId后,可以继续继续调用env->CallXXXMethod来调用,但是如果调用时这个 class 已经被 JVM unload 了,会产生问题。所以最好每次都GetMethodId后再CallXXXMethod.
      • 错误异常处理

        • native 代码调用 JNI 方法时,JNI 方法可能会抛出错误。native 代码应该在调用 JNI 方法后,调用ExceptionOccurred()来检查是不是发生了错误,获取 pendingException,并且做相应的处理。如果没有处理 pendingException 就调用其他的 JNI 方法,即有可能会发生错误,只有少数几个 JNI 方法在有 pendingException 的情况下可以正常运行

        • native 代码调用 JNI 方法ExceptionClear()来表示 pendingException 已经被处理了。

        • native 代码 return 的时候,有未处理的 pendingException,会在 java 代码里 raise this pendingException

        • native 代码可以调用 JNI 方法ThrowNew(JNIEnv*, exceptionClassObject, exceptionMsgString)来主动 raise an exception.

        • 多线程时的异常处理。?一个线程需要在适当的时候调用ExceptionOccurred()来获取是否另一个线程有异常发生,这里我不能很好的理解。

    • JNI 数据类型

      • 基础类型
        • boolean -> jboolean;
        • byte -> jbyte
        • ...
      • 引用类型
        • jobject / jclass / jstring / jarray / jXXXarray / jthrowable
        • C++里面这些类型有父子继承关系,C 里面都是 jobject.
      • type signature
        • 基础类型:Zboolean Bbyte Cchar Sshort Iint Jlong Ffloat Ddouble
        • 类:L fully-qualified-class ;,比如Ljava/lang/String;
        • 数组:[type,比如[B
        • 方法: ( arg-types ) ret-type ,比如 (ILjava/lang/String;[I)J
      • Modified UTF-8 String
        • 一个 UTF-8 字符占 2 个 byte,Modified UTF-8 字符根据 UTF-8 的字符代码,占据 1 个 byte(ASCII),或者两个,或者三个。这里涉及到了编码的设计。
        • 这个 UTF-8 的设计可以使得 ASCII 字符只占据一个 byte,正式 UTF-8 占两个 byte,对于 ASCII 字符,节省空间。
    • JNI Functions

      • JNIEnv*是一个指针,指向一个包含所有 JNI 方法指针的struct
      • functions 分类具体的 jni functions.
        • Version Information
        • Class Operations
        • Exceptions
        • Global and Local References
        • Weak Global References
        • Object Operations
        • Accessing Fields of Objects
        • Calling Instance Methods
        • Accessing Static Fields
        • Calling Static Methods
        • String Operations
        • Array Operations
        • Registering Native Methods
        • Monitor Operations
        • NIO Support
        • Reflection Support
        • Java VM Interface
    • Invocation API 。 用来给独立的 native 代码(即不是从 java 的System.loadLibrary加载的 native 代码)操作 JVM 的 API

      • 可以主动新建一个JVM;让 JVM 加载一个指定的class;执行类的某些方法或者进行某些操作(就像一般的 native 代码执行 jvm 方法一样)。

      • JVM 加载 native library

        • jdk1.2 后,native library 跟自己类所在的 classLoader 绑定。一旦所在类的 classLoader 被卸载了,native library 也会被清除;一个 JVM 只能加载一个 native library 一次
        • native library 可以提供一个方法jint JNI_OnLoad(JavaVM *vm, void *reserved);System.loadLibrary时 JVM 主动调用,以获取 native library 要求的 JVM 版本号(比如JNI_VERSION_1_2这些都是已定义好的int常量)。
        • native library 可以提供一个方法void JNI_OnUnload(JavaVM *vm, void *reserved); 在包含 native library 的 class loader 被 gc 的时候由 JVM 主动调用,以让 native code 执行一些必要的内存清理工作(比如释放 global reference 等)。
      • Invocation API functions native 可以用来主动操作 JVM 的方法(全局方法,不需要调用env->XXX,需要在 native code 里#include <jni.h>)。

        • jint JNI_GetDefaultJavaVMInitArgs(void *vm_args); native code 调用这个方法来获取 JVM 的默认配置参数,vm_args是一个指向JavaVMInitArgs结构的指针。
        // JavaVMInitArgs结构
        typedef struct JavaVMInitArgs {
            jint version;
        
            jint nOptions;
            JavaVMOption *options;
            jboolean ignoreUnrecognized;
        } JavaVMInitArgs;
        
        // JavaVMOption
        typedef struct JavaVMOption {
            char *optionString;  /* the option as a string in the default platform encoding */
            void *extraInfo;
        } JavaVMOption;
        
        // JavaVM结构
        typedef const struct JNIInvokeInterface *JavaVM;
        const struct JNIInvokeInterface ... = {
            NULL,
            NULL,
            NULL,
        
            DestroyJavaVM,
            AttachCurrentThread,
            DetachCurrentThread,
        
            GetEnv,
        
            AttachCurrentThreadAsDaemon
        };
        
        
        • 从 JDK1.2 开始,不支持一个进程里有多个 JVM 了。所以下面的几个方法都受到影响(比如只能获得一个 JVM,或者是)

          • jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs); 获取创建的所有 JVM,放到vmBuf里。
          • jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args); 新建一个JavaVM,native code 所在的线程将会是 JVM 的主线程,参数p_env会用来放 JVM 主线程的JNI Interface,参数vm_args是指向JavaVMInitArgs结构的指针。
        • Java VM结构方法表中的方法:

          • jint DestroyJavaVM(JavaVM *vm);将当前线程 attatch 到 JVM 上,在该线程成为 JVM 的唯一用户线程时,退出该 JVM 并且释放其占有的资源。
          • jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args); 把当前线程加到 JVM 里,参数p_env接收加进去以后的JNI Interface的指针,thr_args是增加线程到 JVM 的参数,结构如下:
          typedef struct JavaVMAttachArgs {
              jint version;  /* must be at least JNI_VERSION_1_2 */
              char *name;    /* the name of the thread as a modified UTF-8 string, or NULL */
              jobject group; /* global ref of a ThreadGroup object, or NULL */
          } JavaVMAttachArgs
          
          • jint AttachCurrentThreadAsDaemon(JavaVM* vm, void** p_env, void* args); 把当前线程作为 Daemon thread加到 JVM 里。如果已经加过了,单纯地设置p_env的值,不会改变已经添加的线程的 daemon 状态(即如果之前不是 daemon,调用这个方法并不会让其变成 daemon)
          • jint DetachCurrentThread(JavaVM *vm); 取消 JVM 里的当前线程,其占有的锁都会释放掉。
          • jint GetEnv(JavaVM *vm, void **env, jint version);获取 JVM 中当前线程对应的JNI Interfaceversion参数为要求的 JVM version,如果实际的 JVM 不支持指定的 version 的话(比如实际为1.1要求的却是1.2),会返回错误,

Java 命令行中使用 jni

  1. 编写 java/kt 代码,注册 native 方法,在static代码块中执行System.loadLibrary(对于 kt 为 companion object 的 init block).
  2. 通过 java/kt 代码生成 class 文件
    • 使用javac或者kotlinc(在 AS 的 plugins 中有该工具),生成.class.
  3. 使用javah X.class对生成的.class文件,生成所需的 C header file , .h
  4. 编写 .h 对应的 .c 文件,在其中实现方法声明的方法。
  5. 调用 gcc -c X.c 来生成 .o 文件
  6. 调用 gcc -shared -o X.so X.o 来生成 .so文件,得到共享库。(在 linux 上为libXXX.so,在 Mac 上为libXXX.jnilib
  7. 调用 java 执行有 jni 参与的 java 类。
    • 使用-Djava.library.path=""来引用所有的 jni 库
    • 使用-cp来指定所需的 class 或者 jar.
    • java_command.md

问题

何时加载 so

在调用 native 方法前的任何时间都可以.通常在类的 static 代码块中进行加载.

jni 方法是如何进行注册的.

  • 静态注册 : 通过 javah 生成 .h 文件,实现其中的方法. 优点: 简单; 缺点 : 方法名长.
  • 动态注册 : 通过在 jNI_OnLoad 方法中调用 JNIEnv.registerNatives 来进行注册,其中参数有 java 方法名c 方法指针 的对应.

jni 的 java 层和 c 层的参数类型如何转换? Integer 会转成什么类型? string 呢?

  • 除了 string/class/Throwable 外的 Object 都转成 jobject.
  • Integer 是 jobject , string 是 jstring

JNIEnv 是线程相关的吗?

  • 是的, JNIEnv 是线程独立的.

JNI 如何在 native 调用 java 的方法? 如何获取一个对象的属性?

  • 找到 jclass -> 通过 jniEnv.getMethodId(jclass, methodName, methodSig) 获取 jMethodID -> 通过 jniEnv.callVoidMethod(obj, methodId, params) ;
    对于 static 方法要使用 jniEnv.callStaticVoidMethod (可能是因为涉及到方法的分派)
  • 对象的成员变量需要用 getByyteField 等方法来获取.

JNI 基础用法相关总结

上一篇:【Django】restframework的外键序列化


下一篇:webpack-使用Vue的配置过程