Android JNI编程:JNIEnv获取与使用技巧 | 实践指南

Android JNI编程:JNIEnv获取与使用技巧 #

引言 #

在Android Native开发中,正确获取和使用JNIEnv是一个关键技术点。本文将详细介绍如何在不同场景下安全高效地获取JNIEnv,以及相关的最佳实践经验。

背景 #

作者目前在做 Android 项目,但大多数逻辑都会在 Native 层实现,不可避免的需要在 Native 层使用 C++ 去调用 Java 的方法,但是在 Native 层调用 Java 方法就需要 JNIEnv 指针,那如何方便的获取 JNIEnv 的指针呢?

分析 #

如下代码:

JNIEXPORT void Java_com_Activity_testEnv( JNIEnv* env, jobject obj) {
   g_obj = env->NewGlobalRef(obj);
}

我们平时可能都见过这种代码,Java 层定义了 Native 的 testEnv 方法,在 Native 层就有一个相应的方法与之对应,同时带有 JNIEnv* 和 jobject 的参数(在 static 的 native 方法中会是 jclass 类型的参数),但是如果这种代码呢?

JNIEXPORT void Java_com_Activity_testEnv(JNIEnv* env, jobject obj) {
    g_obj = env->NewGlobalRef(obj);
    func1(env);
    func2(env);
    func3(env);
    func4(env);
    func5(env);
    func6(env);
    func7(env);
    func8(env);
    func9(env);
}

定义的每个函数都需要将 JNIEnv* 作为参数传递,如果函数内还有很多嵌套,这种方式简直就是灾难,都需要将 JNIEnv * 作为参数传递?是不是很麻烦?你可能有这样的想法,我们把 env 存到本地不就可以了吗,答案是不可以,因为每一个 Java 线程都会有一个对应的 env,我们在 Native 层无法感知到是哪一个 Java 线程,保存的 env 可能当时有效,换一个线程就会失效,而且 Native 层的函数也可以是从 Native 线程(即 pthread 创建的线程)调用,与 Java 线程没有关联,保存的 env 必然是失效的,那怎么办呢?

解决:使用 JavaVM #

这里先介绍下 JNIEnv 和 JavaVM 的概念。

JavaVM #

Java 虚拟机在 Native 层的代表,在 Android 中一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM。

JNIEnv #

Java 调用 Native 语言的环境,是一个封装了几乎所有 JNI 方法的指针,每一个 Java 线程都有一个对应的 JNIEnv,JNIEnv 只在当前线程可用,不能跨线程使用,不同线程的 JNIEnv 彼此独立。在 Native 环境中创建的线程,如果需要调用 JNI 方法,必须要调用 AttachCurrentThread() 与 JVM 进行关联,使用后也需要调用 DetachCurrentThread() 来解除关联。

小总结 #

在 Android 进程中,在 Native 层,通过任何一个可用的 JNIEnv 都可以获取到整个进程唯一的 JavaVM,在任何线程中都可以通过 JavaVM 获取当前线程可用的 JNIEnv,如果是 Native 线程还需要额外与 JVM 进行关联。

到这里大家可能都清楚了,只要能够得到 JavaVM 就可以解决 JNIEnv 的问题,那如何获取 JavaVM 呢?

如何获取 JavaVM? #

这里只介绍 Android 中常见的获取 JavaVM 的方法。

方法一 #

在 Android 中调用 Native 方法前通常都会先加载 Native 的动态链接库,通常都是使用这种方法:

System.loadLibrary(xxx);

这个方法调用后 Native 层会自动调用 JNI_OnLoad 方法:

JavaVM *global_jvm;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    global_jvm = vm;
}

这样 JavaVM 就已经获取到啦,将其保存起来即可。

方法二 #

通过 JNIEnv 获取 JavaVM,在程序的最开始写一个类似于初始化功能的函数,传到 Native 层一个可用的 JNIEnv,之后就可以获取到 JavaVM。

JavaVM *global_jvm;
void get_jvm(JNIEnv *env) {
   env->GetJavaVM(&global_jvm);
}

如何通过 JavaVM 获取 JNIEnv? #

直接看代码:

JNIEnv *get_env(int *attach) {
   if (global_jvm == NULL) return NULL;

   *attach = 0;
   JNIEnv *jni_env = NULL;

   int status = global_jvm->GetEnv((void **)&jni_env, JNI_VERSION_1_6);
   if (status == JNI_EDETACHED || jni_env == NULL) {
       status = global_jvm->AttachCurrentThread(&jni_env, NULL);
       if (status < 0) {
           jni_env = NULL;
       } else {
           *attach = 1;
       }
   }
   return jni_env;
}

void del_env() {
   return global_jvm->DetachCurrentThread();
}

通过前面保存的 JavaVM 就可以获取到 JNIEnv,注意 get_env 函数有一个参数 attach,attach 是一个出参,这个参数返回 1 时,代表当前线程是 Native 线程,使用完后需要调用 del_env() 断开与 JVM 的链接。

使用方法如下:

jobject new_global_object(jobject obj) {
   int attach = 0;
   JNIEnv *env = get_env(&attach);
   jobject ret = env->NewGlobalRef(obj);
   if (attach == 1) {
       del_env();
   }
   return ret;
}

使用这种方式后,我们再也不用被如何获取 JNIEnv 的问题困扰啦。

参考资料 #