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 的问题困扰啦。