[关闭]
@cxm-2016 2016-11-15T16:02:27.000000Z 字数 6095 阅读 18557

JNI完全指南(十)——JavaVM与JNIEnv

JNI完全指南

版本:1
作者:陈小默
声明:禁止商业,禁止转载

发布于:作业部落简书CSDN博客


上一篇:JNI完全指南(九)——反射



十、JavaVM与JNIEnv

10.1 JNIEnv

JNIEnv类型是一个指向全部JNI方法的指针。该指针只在创建它的线程有效,不能跨线程传递。其声明如下:

  1. struct _JNIEnv;
  2. struct _JavaVM;
  3. typedef const struct JNINativeInterface* C_JNIEnv;
  4. #if defined(__cplusplus)
  5. typedef _JNIEnv JNIEnv;
  6. typedef _JavaVM JavaVM;
  7. #else
  8. typedef const struct JNINativeInterface* JNIEnv;
  9. typedef const struct JNIInvokeInterface* JavaVM;
  10. #endif

仅从这一部分我们可以看出的是对于JNIEnv在C语言环境和C++语言环境中的实现是不一样的。也就是说我们在C语言和C++语言中对于JNI方法的调用是有区别的。在C环境下其中方法的声明方式为:

  1. struct JNINativeInterface {
  2. ...
  3. jint (*GetVersion)(JNIEnv *);
  4. ...
  5. };

于是我们在使用此方法的方式为:

  1. jint version = (*env)->GetVersion(env);

对于C++而言,这里进行了封装,其声明为

  1. struct _JNIEnv {
  2. const struct JNINativeInterface* functions;
  3. #if defined(__cplusplus)
  4. jint GetVersion()
  5. { return functions->GetVersion(this); }
  6. ...
  7. #endif /*__cplusplus*/
  8. };

这里可以看出,在C++环境的情况下,这里的使用方式为:

  1. jint version = env->GetVersion();

剩余的方法关于C和C++实现的差别基本如此。我们可以通过以下方法获取当前的JNI版本:

  1. jint GetVersion(JNIEnv *env);

这里的返回值是宏定义的常量,我们可以使用获取到的值与下列宏进行匹配来知道当前的版本:

  1. #define JNI_VERSION_1_1 0x00010001
  2. #define JNI_VERSION_1_2 0x00010002
  3. #define JNI_VERSION_1_4 0x00010004
  4. #define JNI_VERSION_1_6 0x00010006

10.2 JavaVM

JavaVM是虚拟机在JNI中的表示,一个JVM中只有一个JavaVM对象,这个对象是线程共享的。

通过JNIEnv我们可以获取一个Java虚拟机对象,其函数如下:

jint GetJavaVM(JNIEnv *env, JavaVM **vm);

  • vm:用来存放获得的虚拟机的指针的指针。
  • return:成功返回0,失败返回其他。

我们来看一下在C语言环境下,JNI中JVM的声明:

  1. /*
  2. * JNI invocation interface.
  3. */
  4. struct JNIInvokeInterface {
  5. void* reserved0;
  6. void* reserved1;
  7. void* reserved2;
  8. jint (*DestroyJavaVM)(JavaVM*);
  9. jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
  10. jint (*DetachCurrentThread)(JavaVM*);
  11. jint (*GetEnv)(JavaVM*, void**, jint);
  12. jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
  13. };

JNI中操作JVM的声明如下:

  1. jint JNI_GetDefaultJavaVMInitArgs(void*);
  2. jint JNI_CreateJavaVM(JavaVM**, JNIEnv**, void*);
  3. jint JNI_GetCreatedJavaVMs(JavaVM**, jsize, jsize*);
  4. JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
  5. JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);

10.2.1 创建JVM

一般而言,调用JNI_CreateJavaVM创建JVM的线程被称为主线程。理论上来说,此方法不允许用户调用。

jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);

  • p_vm:保存创建的虚拟机的指针。
  • p_env:保存获得到的JNIEnv对象的指针。
  • vm_args:一个JavaVMInitArgs类型的指针,用来设置初始化参数。
  • return:创建成功返回JNI_OK,失败返回其他。

其中JavaVMInitArgs是存放虚拟机参数的结构体,定义如下:

  1. typedef struct JavaVMOption {
  2. const char* optionString;
  3. void* extraInfo;
  4. } JavaVMOption;
  5. typedef struct JavaVMInitArgs {
  6. jint version;
  7. jint nOptions;
  8. JavaVMOption* options;
  9. jboolean ignoreUnrecognized;
  10. } JavaVMInitArgs;

以下举例说明创建过程:

  1. JavaVMInitArgs vm_args;
  2. JavaVMOption options[4];
  3. options[0].optionString = "-Djava.compiler=NONE"; /* disable JIT */
  4. options[1].optionString = "-Djava.class.path=c:\myclasses"; /* user classes */
  5. options[2].optionString = "-Djava.library.path=c:\mylibs"; /* set native library path */
  6. options[3].optionString = "-verbose:jni"; /* print JNI-related messages */
  7. vm_args.version = JNI_VERSION_1_2;
  8. vm_args.options = options;
  9. vm_args.nOptions = 4;
  10. vm_args.ignoreUnrecognized = TRUE;
  11. /* Note that in the JDK/JRE, there is no longer any need to call
  12. * JNI_GetDefaultJavaVMInitArgs.
  13. */
  14. res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
  15. if (res < 0) ...

10.2.2 链接到虚拟机

JNIEnv指针仅在创建它的线程有效。如果我们需要在其他线程访问JVM,那么必须先调用AttachCurrentThread将当前线程与JVM进行关联,然后才能获得JNIEnv对象。当然,我们在必要时需要调用DetachCurrentThread来解除链接。

jint AttachCurrentThread(JavaVM* vm , JNIEnv** env , JavaVMAttachArgs* args);

  • vm:虚拟机对象指针。
  • env:用来保存得到的JNIEnv的指针。
  • args:链接参数,参数结构体如下所示。
  • return:链接成功返回0,连接失败返回其他。
  1. struct JavaVMAttachArgs {
  2. jint version; /* must be >= JNI_VERSION_1_2 */
  3. const char* name; /* NULL or name of thread as modified UTF-8 str */
  4. jobject group; /* global ref of a ThreadGroup object, or NULL */
  5. };

10.2.3 解除与虚拟机的连接

下列函数用来解除当前线程与虚拟机之间的链接:

jint DetachCurrentThread(JavaVM* vm);

10.2.4 卸载虚拟机

调用JNI_DestroyJavaVM函数将会卸载当前使用的虚拟机。

jint DestroyJavaVM(JavaVM* vm);

10.2.5 动态加载本地方法

在JNI中有一组特殊的函数:

  1. JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
  2. JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);

这一组函数的作用就是负责Java方法和本地C函数的链接。例如,我在Java代码中声明了这样一段本地代码:

  1. package com.github.cccxm;
  2. class NativeLib{
  3. public static native String getName(int number);
  4. }

一般情况下,我们需要在本地源文件中声明如下:

  1. JNIEXPORT jstring JNICALL Java_com_github_cccxm_NativeLib_getName(JNIEnv *env,jobject thiz,jint number);

那么,Java方法和本地函数之间的映射关系编译器已经帮我们做了。如果在某些场景下,我们需要动态地加载本地方法。例如,我们现在仍使用NativeLib类,但是在本地代码中,我们声明了一个没有按照JNI规范命名的本地函数:

  1. JNIEXPORT jstring JNICALL getName(JNIEnv *env, jclass clazz);

那么我们就必须使用动态关联的方式实现Java方法与本地函数的映射,代码如下:

  1. extern "C"
  2. JNIEXPORT jstring JNICALL getName(JNIEnv *env, jobject thiz, int number) {
  3. ALOGE("number is %d",number);
  4. return env->NewStringUTF("hello world");
  5. }
  6. static const char *CLASS_NAME = "com/github/cccxm/NativeLib";//类名
  7. static JNINativeMethod method = {//本地方法描述
  8. "getName",//Java方法名
  9. "(I)Ljava/lang/String;",//方法签名
  10. (void *) getName //绑定本地函数
  11. };
  12. static bool
  13. bindNative(JNIEnv *env) {
  14. jclass clazz;
  15. clazz = env->FindClass(CLASS_NAME);
  16. if (clazz == NULL) {
  17. return false;
  18. }
  19. return env->RegisterNatives(clazz, &method, 1) == 0;
  20. }
  21. JNIEXPORT jint JNICALL
  22. JNI_OnLoad(JavaVM *vm, void *reserved) {
  23. JNIEnv *env = NULL;
  24. jint result = -1;
  25. if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
  26. return result;
  27. }
  28. bool res = bindNative(env);
  29. ALOGE("bind result is %s",res?"ok":"error");
  30. // 返回jni的版本
  31. return JNI_VERSION_1_6;
  32. }

注意:JNI_OnLoad方法在每一个.so库中只能存在一个。

10.2.6 卸载本地方法

在上面的例子中我们了解了如何动态加载一个本地方法,那么有加载就有卸载,接下来,我们看一下如何卸载一个本地方法。JNI_OnLoad方法是在动态库被加载时调用,而JNI_OnUnload则是在本地库被卸载时调用。所以这两个函数就是一个本地库最重要的两个生命周期方法。如果没有显式的卸载一个本地库则不会看到此方法被调用。以下举例说明用法:

  1. ...
  2. static bool
  3. unBindNative(JNIEnv *env) {
  4. jclass clazz;
  5. clazz = env->FindClass(CLASS_NAME);
  6. if (clazz == NULL) {
  7. return false;
  8. }
  9. return env->UnregisterNatives(clazz) == 0;
  10. }
  11. JNIEXPORT void JNICALL
  12. JNI_OnUnload(JavaVM *vm, void *reserved) {
  13. JNIEnv *env = NULL;
  14. jint result = -1;
  15. if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
  16. return;
  17. }
  18. bool res = unBindNative(env);
  19. ALOGE("unbind result is %s", res ? "ok" : "error");
  20. }

10.2.7 获取默认虚拟机初始化参数

通过以下函数能够获取到默认的虚拟机初始化参数:

jint JNI_GetDefaultJavaVMInitArgs(void *vm_args);

  • vm_args:JavaVMInitArgs类型的参数,该结构体声明在10.2.1
  • return:获取成功返回JNI_OK,失败返回其他。

10.2.8 获取Java虚拟机

通过以下方法可以获取到已经被创建的Java虚拟机对象。

jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);

  • vmBuf:用来保存Java虚拟机的缓冲区
  • bufLen:缓冲区长度。
  • nVms:实际获得到的Java虚拟机个数。
  • return:获取成功返回JNI_OK,失败返回其他。

后记

虽然整个系列内容并不多,但是中途任然好几次的想放弃,看来写系列博客并不是一件什么轻松的事。

这里送上一首颜真卿的诗,与君共勉

三更灯火五更鸡,
正是男儿读书时。
黑发不知勤学早,
白首方悔读书迟。

本系列完结,感谢各位读者的大力支持。其中内容会不断补充。如有不当之处,恳请批评指教。


[1]ORACLE guides for JNI——Chapter 4: JNI Functions

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注