JNI: 连接Java世界的JavaVM和JNIEnv

栏目: IOS · 发布时间: 4年前

内容简介:在当 Java 层访问 Nativce 层的时候会自动在

不使用IDE做一次JNI开发 一文中,我们做了一次从 Java 层到 Native 层的开发。那么,我们能不能反过来,完成一次从 Native 层到 Java 层的开发呢?当然能,不过过程可没那么简单,而掌握 JavaVMJNIEnv 这两个结构体就是关键,这两个结构体就是通往 Java 世界的大门,重要性不言而喻。

JavaVM

JavaVM 这个结构体指针在简单的 JNI 开发中很少使用到,它是虚拟机的代表,从 JDK 1.2 开始,一个进程只允许创建一个虚拟机。

当 Java 层访问 Nativce 层的时候会自动在 JNI 层创建一个 JavaVM 指针,而我们在 JNI 层通常所使用的都是从 JavaVM 中获取的 JNIEnv 指针。那么现在我们来看下 JavaVM 这个结构体

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
};
复制代码

本文分析的代码都是 C++ 版本,因为 C++ 版本的 JNI 使用起来方便些。

可以看到所有方法都是由结构体 JNIInvokeInterface 实现的,那么来看下这个结构体吧

/*
 * JNI invocation interface.
 */
struct JNIInvokeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;

    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};
复制代码

非常简单,前三个指针作为保留使用,后五个指针为函数指针,从函数指针的名字可以推测出函数的用途,其中 DestoryJavaVM 函数是用来销毁虚拟机的,  getEnv 函数是用来获取 JNIEnv 指针的。后面会举例解释这几个函数用途。

创建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 头文件中。

参数解释

  1. p_vm : 是一个指向 JavaVM * 的指针,函数成功返回时会给 JavaVM * 指针赋值。
  2. p_env : 是一个指向 JNIEnv * 的指针,函数成功返回时会给 JNIEnv * 指针赋值。
  3. vm_args : 是一个指向 JavaVMInitArgs 的指针,是初始化虚拟机的参数。

如果函数执行成功,返回 JNI_OK (值为0),如果失败返回负值。

基本上可以这样理解 JNI_CreateJavaVM() 函数,它就是为了给 JavaVM * 指针 和 JNIEnv * 指针赋值。我们得到这两个指针便可以操纵"万物",这里的"万物"指的是 Java 世界的"万物"。

使用 JavaVM

那么我们如何使用这个函数呢?这个还是有点小复杂的,需要对 Java虚拟机 有比较深的认知。那么我们只能找个例子来学习,找哪个例子呢?在 Android 源码中,Zygote 进程开启 Java 世界就是一个绝佳的例子。

Zygote 进程启动入口为 App_main.cppmain() 函数

int main(int argc, char* const argv[])
{
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
    runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
}
复制代码

代码是简化的,只是为了学习 JNI 而已。

main() 函数最终会调用 AppRuntime 类的 start() 函数, AppRuntime 还是定义在 App_main.cpp 文件中,它是 AndroidRuntime 的子类,并且 start() 方法是由 AndroidRuntime 类实现的。

那么,现在看下 AndroidRuntime.cppstart() 函数实现

JavaVM* AndroidRuntime::mJavaVM = NULL;

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    // 1. 创建虚拟机
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote) != 0) {
        return;
    }
    
    // 2. 注册函数
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }
    
    // 3. 调用ZygoteInit.java的main()方法
    env->CallStaticVoidMethod(startClass, startMeth, strArray);
    
    // 4. 从JavaVM中分离当前线程
    if (mJavaVM->DetachCurrentThread() != JNI_OK)
        ALOGW("Warning: unable to detach main thread\n");
    // 5. 销毁JavaVM
    if (mJavaVM->DestroyJavaVM() != 0)
        ALOGW("Warning: VM did not shut down cleanly\n");
}
复制代码

第一步创建虚拟机就是调用了 JNI_CreateJavaVM() 函数,函数成功返回后,就可以得到赋值的 JavaVM * 指针和 JNIEnv * 指针,也就是代码中的全局变量 mJavaVM 和 局部变量 env 。这里,需要注意下为何 mJavaVM 是全局变量,而 env 是局部变量?这个后面讲 JNIEnv 的时候会解释。

第二步,第三步就是利用获取的 JNIEnv * 来访问 Java 世界。

第四步调用 JavaVMDeatchCurrentThread() 函数,这个函数从命名就可以看出是从虚拟机分离当前线程。此时,我们应该想到了 JavaVM 的另外一个函数 AttachCurrentThread() ,这个函数是把当前线程附着到虚拟机中。然而我们并没有调用附着的操作,怎么就直接出现 DeatchCurrentThread() 函数呢? 那是因为 JNI_CreateJavaVM() 直接把当前线程附着到了虚拟机中。这个在后面讲 JNIEnv 也会有解释。

第五步调用 JavaVMDestroyJavaVM() 函数,销毁虚拟机,

JNIEnv

_JavaVM 结构体中有一个函数 getEnv() ,与之相对应的函数原型如下

jint GetEnv(JavaVM *vm, void **env, jint version);
复制代码

参数说明

  1. vm: 虚拟机对象。
  2. env: 一个指向 JNIEnv 结构的指针的指针。
  3. version: JNI版本,根据jdk的版本,目前有四种值,分别为 JNI_VERSION_1_1 , JNI_VERSION_1_2 , JNI_VERSION_1_4 , JNI_VERSION_1_6

这个函数执行结果有几种情况:

  1. 如果当前线程没有附着到虚拟机中,也就是没有调用 JavaVM  的 AttachCurrentThread() 函数,那么就会设置 *env  的值为 NULL ,并且返回 JNI_EDETACHED (值为-2)。
  2. 如果参数 version 锁指定的版本不支持,那么就会设置 *env 的值为 NULL ,并且返回 JNI_EVERSION (值为-3)。
  3. 除去上面的两种异常情况,就会给 *env 设置正确的值,并且返回 JNI_OK (值为0)。

JNIEnv使用限制

函数执行结果的第一种情况来可以说明几个问题

  1. JNIEnv * env  是与线程相关,因此多个线程之间不能共享同一个 env
  2. 如果在Native层新建一个线程,要获取 JNIEnv * env ,那么必须做到如下两点
    • 线程必须调用 JavaVMAttachCurrentThread() 函数。
    • 必须全局保存 JavaVM * mJavaVm ,那么就可以在线程中通过调用 JavaVMgetEnv() 函数来获取 JNIEnv * env 的值。

我们还记得 JNI_CreateJavaVM()  函数也设置 *env  的值吗?那么它肯定也会执行 AttachCurrentThread() 函数把当前线程附着到虚拟机中。这也就是解释了为何在没有明显调用 AttachCurrentThread() 的情况下,可以执行 JavaVMDetachCurrentThread() 函数。

JNIEnv作用

GetEnv() 函数从命名可以看出是给 JNIEnv *env  赋值的,那么这个 JNIEnv 又有什么作用呢?来看下结构体声明吧

struct _JNIEnv {
    const struct JNINativeInterface* functions;

    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }
    
    jobject NewGlobalRef(jobject obj)
    { return functions->NewGlobalRef(this, obj); }
    
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }
    
    // ... 省略无数个函数
}    
复制代码

从声明可以看出, _JNIEnv_JavaVM 玩了一样的套路,函数的都是交由另外一个指针实现,而这里就是交给 JNINativeInterface  结构体指针,从结构体命名可以大致猜下意思,它应该是定义了JNI函数调用的接口,究竟是不是呢,看下结构体声明

/*
 * Table of interface function pointers.
 */
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;
    
    jclass      (*FindClass)(JNIEnv*, const char*);
    jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    
    // 调用返回值为int类型的Java方法
    jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
    
    // 获取Java对象某个变量的值
    jboolean    (*GetBooleanField)(JNIEnv*, jobject, jfieldID);
    // 设置Java对象某个变量的值
    void        (*SetBooleanField)(JNIEnv*, jobject, jfieldID, jboolean);
    
    // 创建一个Java的String对象
    jstring     (*NewStringUTF)(JNIEnv*, const char*);
    
    // ... 省略无数个操作Java的方法
}    
复制代码

从函数的作用来看,原来这个结构体是操作 Java 层的入口,从这里就可见 JNIEnv * 指针的作用了,而这个指针正是本地函数的第一个参数,例如在 不使用IDE做一次JNI开发 一文中实现的一个本地函数如下

extern "C" JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI (JNIEnv * env, jclass clazz)
{
    const char * str_hello = "Hello from C++";
    return env->NewStringUTF(str_hello);
}
复制代码

注意看第一个参数 JNIEnv * env ,这是自动虚拟机自动帮我们传入的,意思就是告诉你,可以利用这个指针来操作 Java 层。

总结

通过对 _JavaVM_JNIEnv 结构的了解,我们就知道利用这两个结构体指针是可以打通 Java 世界的,而具体操纵 Java 世界的是 JNIEnv * 指针,那么具体如何操作 Java 世界"万物"呢,后面文章会一一详述。


以上所述就是小编给大家介绍的《JNI: 连接Java世界的JavaVM和JNIEnv》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JAVASCRIPT权威指南(第四版)

JAVASCRIPT权威指南(第四版)

David Flanagan / 张铭泽、等 / 机械工业出版社 / 2003-1-1 / 99.00

《JavaScript权威指南》全面介绍了JavaScript语言的核心,以及Web浏览器中实现的遗留和标准的DOM。它运用了一些复杂的例子,说明如何处理验证表单数据、使用cookie、创建可移植的DHTML动画等常见任务。本书还包括详细的参考手册,涵盖了JavaScript的核心API、遗留的客户端API和W3C标准DOM API,记述了这些API中的每一个JavaScript对象、方法、性质、......一起来看看 《JAVASCRIPT权威指南(第四版)》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具