JNI
Sun JNI
-
JNI overview
- 为什么需要 jni?
- 有了标准以后,native 库可移植。
- 统一 java 和 native 的互操作接口,使得这个接口不会受具体的 JVM 实现的影响。早期有一些 JVM 规定了私有的与 native 的交互,JVM 之间的 native 操作不兼容。
- 一些时间要求严格的操作,使用低层次 native 代码进行,牺牲高级语言的方便,提高性能。
- 有了标准以后,native 库可移植。
- 为什么需要 jni?
-
JNI 知识脉络
- 与 java 的交互
- java 调用 native
- jni 方法 load
- 数据类型 / jni 方法格式
- native 调用 java
- jniEnv 提供的 API
- 反射 : 真的反射 + (类 / 对象/string/array + 方法 / 属性)
- 锁,JVM 信息
- jniEnv 提供的 API
- java 调用 native
- 与 java 的交互
-
JNI design
-
JNI 暴露给 native 的是一个 Pointer.Java 调用 native 的方法时,这个 Pointer 作为一个参数(即 JNIEnv,提供了很多操作 JVM 的方法,这些方法叫作 JNI 方法)
- Pointer->Pointer->Pointer 是一个指针数组,每个指针指向一个方法。
-
编译/ 链接 /加载
- java 代码使用
System.loadLibrary
来加载 native 代码到内存中。JVM 内部为每一个classLoader
维护一个已加载的 native library list.
- java 代码使用
-
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。所以
- 对 JVM 来说:JVM 必须对传入 native 的对象引用(reference)做额外的计数,才能保证这些对象不被 gc 清除。
- 对 native 代码来说:native 代码必须在不需要引用后,主动通知 JVM。
-
对于 native 代码来说,对象的 reference 有两种:global和local 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 代码开始到 native 代码返回。JVM 在调用 native 方法时主动维护一个 local reference table,保存所有 native 代码引用的 local reference 防止其对象被 gc 掉,在 native 方法结束后将这些 reference 清空,允许 gc 清除。native 代码可以主动调用
- 被 native 代码持有的 local reference 和 global reference 都不会被 gc 掉。
- global reference
-
native 代码获取对象的 property 和调用对象的 method
- 步骤
jmethodID mid = env->GetMethodID(cls, “f”, “(ILjava/lang/String;)D”);
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()
来获取是否另一个线程有异常发生,这里我不能很好的理解。
-
-
-
- 基础类型
- 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 字符,节省空间。
- 基础类型
-
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 Interface
,version
参数为要求的 JVM version,如果实际的 JVM 不支持指定的 version 的话(比如实际为1.1
要求的却是1.2
),会返回错误,
-
-
Java 命令行中使用 jni
- 编写 java/kt 代码,注册 native 方法,在
static
代码块中执行System.loadLibrary
(对于 kt 为 companion object 的init
block). - 通过 java/kt 代码生成 class 文件
- 使用
javac
或者kotlinc
(在 AS 的 plugins 中有该工具),生成.class
.
- 使用
- 使用
javah X.class
对生成的.class
文件,生成所需的 C header file ,.h
- 编写
.h
对应的.c
文件,在其中实现方法声明的方法。 - 调用
gcc -c X.c
来生成.o
文件 - 调用
gcc -shared -o X.so X.o
来生成.so
文件,得到共享库。(在 linux 上为libXXX.so
,在 Mac 上为libXXX.jnilib
) - 调用
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
等方法来获取.