JavaVM 和 JNIEnv
JNI 定义了两个关键数据结构,即JavaVM
和JNIEnv
。两者本质上都是指向函数表的二级指针。在 C++ 版本中,它们是一些类,这些类具有指向函数表的指针,并具有每个通过该函数表间接调用的 JNI 函数的成员函数。掌握 JavaVM
和 JNIEnv
这两个结构体就是关键,这两个结构体就是通往 Java 世界的大门,重要性不言而喻。
JavaVM
JavaVM
这个结构体指针在简单的 JNI
开发中很少使用到,它是虚拟机的代表,从 JDK 1.2
开始,一个进程只允许创建一个虚拟机。
当 Java 层访问 Nativce 层的时候会自动在 JNI
层创建一个 JavaVM
指针,而我们在 JNI
层通常所使用的都是从 JavaVM
中获取的 JNIEnv
指针。那么现在我们来看下 JavaVM
这个结构体
/*
* C++ version.
*/
struct _JavaVM {
const struct JNIInvokeInterface* functions;
#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
JavaVM
是虚拟机在JNI中的表示,一个JVM中只有一个JavaVM
对象,这个对象是线程共享的。
通过JNIEnv我们可以获取一个Java虚拟机对象,其函数如下:
/**
* 获取Java虚拟机对象
* @param env JNIEnv对象
* @param vm 用来存放获得的虚拟机的指针的指针
* @return 成功返回0,失败返回其他
*/
jint GetJavaVM(JNIEnv *env, JavaVM **vm);
在加载动态链接库的时候,JVM
会调用JNI_OnLoad(JavaVM* jvm, void* reserved)
(如果定义了该函数),第一个参数会传入JavaVM
指针。可以在该函数中保存JavaVM
指针来供全局使用。
JavaVM *javaVM = NULL;
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
javaVM = vm;
...
}
创建JavaVM
从 Java 层到 Native 层的开发的时候,我们并不需要手动创建 JavaVM
对象,因此虚拟机自动帮我们完成了这些工作。然而,如果从 Native 层到 Java 层开发的时候,我们就需要手动创建 JavaVM
对象,创建的函数原型如下
#inlcude <jni.h>
jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);
JNI_CreateJavaVM
函数不属于任何结构体,方法声明在jni.h
头文件中。
参数解释
-
p_vm
: 是一个指向JavaVM *
的指针,函数成功返回时会给JavaVM *
指针赋值。 -
p_env
: 是一个指向JNIEnv *
的指针,函数成功返回时会给JNIEnv *
指针赋值。 -
vm_args
: 是一个指向JavaVMInitArgs
的指针,是初始化虚拟机的参数。
如果函数执行成功,返回 JNI_OK
(值为0),如果失败返回负值。
基本上可以这样理解 JNI_CreateJavaVM()
函数,它就是为了给 JavaVM *
指针 和 JNIEnv *
指针赋值。我们得到这两个指针便可以操纵"万物",这里的"万物"指的是 Java 世界的"万物"。
JNIEnv
在 _JavaVM
结构体中有一个函数 getEnv()
,与之相对应的函数原型如下
jint GetEnv(JavaVM *vm, void **env, jint version);
参数说明
- vm: 虚拟机对象。
- env: 一个指向
JNIEnv
结构的指针的指针。 - version: JNI版本,根据jdk的版本,目前有四种值,分别为
JNI_VERSION_1_1
,JNI_VERSION_1_2
,JNI_VERSION_1_4
,JNI_VERSION_1_6
。
这个函数执行结果有几种情况:
- 如果当前线程没有附着到虚拟机中,也就是没有调用
JavaVM
的AttachCurrentThread()
函数,那么就会设置*env
的值为NULL
,并且返回JNI_EDETACHED
(值为-2)。 - 如果参数
version
锁指定的版本不支持,那么就会设置*env
的值为NULL
,并且返回JNI_EVERSION
(值为-3)。 - 除去上面的两种异常情况,就会给
*env
设置正确的值,并且返回JNI_OK
(值为0)。
JNIEnv
类型是一个指向全部JNI方法的指针,JNIEnv
提供了大部分 JNI 函数。JNIEnv
只在创建它的线程有效,<font color='red'>不能跨线程传递</font>,不能再线程之间共享 JNIEnv
。
所有的本地接口函数都会以 JNIEnv
作为第一个参数。不管是静态注册的本地C/C++函数接口,还是动态注册的本地函数接口,函数的第一个参数都是JNIEnv
。
如果一段代码无法通过其他方法获取自己的 JNIEnv
,可以通过全局有效的 JavaVM
,然后使用 GetEnv
来获取当前线程的 JNIEnv
(如果该线程包含一个 JNIEnv
)。
JNIEnv* env = NULL;
if (javaVM->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
GetEnv
函数定义如下:
/**
* 获取当前线程JNIEnv
* @param env 用来存放获取JNIEnv对象的指针的指针
* @param version JNI版本
* @return 成功返回0,失败返回其他
*/
jint GetEnv(void** env, jint version)
对于本地库中创建的线程,需要使用AttachCurrentThread
来附加到 JavaVM
来获取一个可用的JNIEnv
。线程退出或不再需要使用JNIEnv
时,必须通过调用DetachCurrentThread
来解除连接。
JNIEnv使用限制
函数执行结果的第一种情况来可以说明几个问题
-
JNIEnv * env
是与线程相关,因此多个线程之间不能共享同一个env
。 - 如果在Native层新建一个线程,要获取
JNIEnv * env
,那么必须做到如下两点- 线程必须调用
JavaVM
的AttachCurrentThread()
函数。 - 必须全局保存
JavaVM * mJavaVm
,那么就可以在线程中通过调用JavaVM
的getEnv()
函数来获取JNIEnv * env
的值。
- 线程必须调用
我们还记得 JNI_CreateJavaVM()
函数也设置 *env
的值吗?那么它肯定也会执行 AttachCurrentThread()
函数把当前线程附着到虚拟机中。这也就是解释了为何在没有明显调用 AttachCurrentThread()
的情况下,可以执行 JavaVM
的 DetachCurrentThread()
函数。
C语言调用C++调用
JavaVM
和 JNIEnv
在 C 语言环境下和 C++ 环境下调用是有区别的,以NewStringUTF
函数为例:
C语言调用格式为:
(*env)->NewStringUTF(env, “Hellow World!”);
C++调用格式为:
env->NewStringUTF(“Hellow World!”);
建议使用 C++ 格式,这也是大部分代码使用的形式。但C++ 格式其实只是封装了 C 格式,使得调用更加简介方便。