JNI/NDK入门指南之JavaVM和JNIEnv

      JNI/NDK入门指南之JavaVM和JNIEnv

  在前面的章节JNI数据类型,描述符详解中,我们详解了JNI数据类型和描述符的一些概念,那么在今天我们将要熟悉掌握JNI的开发中另外两个关键知识点JavaVMJniEnv



一.细说JavaVM

JavaVM,英文全称是Java virtual machine,用咋中国话来说就是Java虚拟机。一个JVM中只有一个JavaVM对象,这个JavaVM则可以在进程中的各线程间共享的,这个特性在JNI开发中是非常重要的。


1.获取JavaVM虚拟机接口

在JNI的开发中有两种方法可以获取JavaVM,下面来分别介绍一下。
方式一:
在加载动态链接库的时候,JVM会调用JNI_OnLoad(JavaVM* jvm, void* reserved)(如果定义了该函数)。第一个参数会传入JavaVM指针。代码如下:

jint JNI_OnLoad(JavaVM * vm, void * reserved){
	JNIEnv * env = NULL;
	jint result = -1;

	if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
	    LOGE(TAG, "ERROR: GetEnv failed\n");
	    goto bail;
	}
	result = JNI_VERSION_1_4;
	bail:
	return result;

方式二:
在Native code中调用JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args)可以得到JavaVM指针。我们的Android系统是利用第二种方式来创建art虚拟机的的,具体的创建过程我就不想说了,这个不是本文讲解的重点。对于以上两种获取JavaVM的方式,都可以用全局变量,比如JavaVM* g_jvm来保存获得的指针以便在任意上下文中使用。
JNI/NDK入门指南之JavaVM和JNIEnv_第1张图片

方式三:
通过JNIEnv获取JavaVM,具体参考代码如下:

JNIEXPORT void JNICALL Java_com_xxx_android2native_JniManager_openJni
  (JNIEnv * env, jobject object)
{
	LOGE(TAG, "Java_com_xxx_android2native_JniManager_openJni");
	//注意,直接通过定义全局的JNIEnv和Object变量,在此保存env和object的值是不可以在线程中使用的

	//线程不允许共用env环境变量,但是JavaVM指针是整个jvm共用的,所以可以通过下面
	//的方法保存JavaVM指针,在线程中使用
	env->GetJavaVM(&gJavaVM);

	//同理,jobject变量也不允许在线程中共用,因此需要创建全局的jobject对象在线程
	//中访问该对象
    gJavaObj = env->NewGlobalRef(object);

	gIsThreadStop = 0;

	

2.查看JavaVM定义

通过前面的章节我们对JavaVM有了一定的了解,下面让我们看看JNI中对JavaVM的申明,JavaVM申明在jni.h文件里面,这个你一定不会陌生,因为我们在JNI开发中,必定要引入#include 头文件。
C语言中JavaVM声明如下

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;//C语言定义
#endif

/*
 * 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*);
};

C++中JavaVM声明如下

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

/*
 * 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*/
};


二.细说JNIEnv

JNIEnv,英文全称是Java Native Interface Environment,用咋中国话来说就是Java本地接口环境。在进行JNI编程开发的时候,使用javah生成Native方法对应的Native函数声明,会发现所有的Native函数的第一个参数永远是JNIEnv指针,如下所示:

/*
 * Class:     com_xxx_object2struct_JniTransfer
 * Method:    getJavaBeanFromNative
 * Signature: ()Lcom/xxx/object2struct/JavaBean;
 */
JNIEXPORT jobject JNICALL Java_com_xxx_object2struct_JniTransfer_getJavaBeanFromNative
  (JNIEnv *, jclass);

/*
 * Class:     com_xxx_object2struct_JniTransfer
 * Method:    transferJavaBeanToNative
 * Signature: (Lcom/xxx/object2struct/JavaBean;)V
 */
JNIEXPORT void JNICALL Java_com_xxx_object2struct_JniTransfer_transferJavaBeanToNative
  (JNIEnv *, jclass, jobject);

JNIEnv是提供JNI Native函数的基础环境,线程相关,不同线程的JNIEnv相互独立,并且JNIEnv是一个JNI接口指针,指向了本地方法的一个函数表,该函数表中的每一个成员指向了一个JNI函数,本地方法通过JNI函数来访问JVM中的数据结构,详情如下图:
JNI/NDK入门指南之JavaVM和JNIEnv_第2张图片
通过上面的图示,我们应该更加了解JNIEnv只在当前线程中有效。本地方法不 能将JNIEnv从一个线程传递到另一个线程中。相同的 Java 线程中对本地方法多次调用时,传递给该本地方法的JNIEnv是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNIEnv。


1.查看JNIEnv定义

通过前面的章节我们对JavaVM有了一定的了解,下面让我们看看JNI中对JNIEnv的申明,可以看出JNIEnv是一个包含诸多JNI函数的结构体,JNIEnv申明在jni.h文件里面,这个你一定不会陌生,因为我们在JNI开发中,必定要引入#include 头文件。

C语言中JNIEnv声明如下

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;//C的定义
typedef const struct JNIInvokeInterface* JavaVM;
#endif


/*
 * Table of interface function pointers.
 */
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;

    jint        (*GetVersion)(JNIEnv *);
	...
}

在C语言中对JNIEnv下GetVersion()方法使用如下:

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

C++中JNIEnv声明如下

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;//C的定义
typedef const struct JNIInvokeInterface* JavaVM;
#endif


/*
 * C++ object wrapper.
 *
 * This is usually overlaid on a C struct whose first element is a
 * JNINativeInterface*.  We rely somewhat on compiler behavior.
 */
struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); }
    ...
}

在C++中对JNIEnv下GetVersion()方法使用如下:

   jint version = env->GetVersion();

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


2.JNIEnv结构体中JNI函数划分

通过分析前面章节C/C++中JNIEnv结构体的话,不难发现,这个结构体当中包含了几乎有所的JNI函数,大致可以分为如下几类:

函数名 功能
FindClass 该函数用于加载本地定义的类
GetObjectClass 通过对象获取这个类
NewGlobalRef 创建 obj 参数所引用对象的新全局引用
NewObject 构造新 Java 对象
NewString 利用 Unicode 字符数组构造新的 java.lang.String 对象
NewArray 创建类型为Type的数组对象
GetField 获取类型为Type的字段
SetField 设置类型为Type的字段的值
GetStaticField 获取类型为Type的static的字段
SetStaticField 设置类型为Type的static的字段的值
CallMethod 调用返回类型为Type的方法
CallStaticMethod 调用返回值类型为Type的static方法

常见的JNI函数还有一些,这里由于篇幅问题就不过多介绍了。这里推荐一个博客JNI学习积累之一 ---- 常用函数大全里面有比较详细的描述了,大家可以仔细阅读。当然最好的办法,就是实际使用中慢慢品尝了。


3.获取JNIEnv

如果是在同一个线程中需要使用JNIEnv,这个通过前面的讲解我想读者朋友们一定会脱口而出,使用参数传递,是的这个是可以做到的。但是使用跨线程呢?这个一般会使用到全局引用了,参加如下代码,具体可以参见我的博客Android和C/C++通过Jni实现通信方式一中对于跨线程使用JNIEnv有比较详细的介绍了。



static void* native_thread_exec(void *arg)
{
    LOGE(TAG,"nativeThreadExec");
	LOGE(TAG,"The pthread id : %d\n", pthread_self());
    JNIEnv *env;
    //从全局的JavaVM中获取到环境变量
    gJavaVM->AttachCurrentThread(&env,NULL);
	
    //get Java class by classPath
    //获取Java层对应的类
    jclass thiz = env->GetObjectClass(gJavaObj);
	
    //get Java method from thiz
    //获取Java层被回调的函数
    jmethodID nativeCallback = env->GetMethodID(thiz,"callByJni","(I)V");
    int count = 0;

	//线程循环
    while(!gIsThreadStop)
    {
        sleep(2);
		//跨线程回调Java层函数
        env->CallVoidMethod(gJavaObj,nativeCallback,count++);
    }
    gJavaVM->DetachCurrentThread();
    LOGE(TAG,"thread stoped");
	return ((void *)0);
}
JNIEXPORT void JNICALL Java_com_xxx_android2native_JniManager_openJni
  (JNIEnv * env, jobject object)
{
	LOGE(TAG, "Java_com_xxx_android2native_JniManager_openJni");
	//注意,直接通过定义全局的JNIEnv和Object变量,在此保存env和object的值是不可以在线程中使用的

	//线程不允许共用env环境变量,但是JavaVM指针是整个jvm共用的,所以可以通过下面
	//的方法保存JavaVM指针,在线程中使用
	env->GetJavaVM(&gJavaVM);

	//同理,jobject变量也不允许在线程中共用,因此需要创建全局的jobject对象在线程
	//中访问该对象
    gJavaObj = env->NewGlobalRef(object);

	gIsThreadStop = 0;
}


三.Java和Android中JavaVM对象有区别

在Java里,每一个Process可以产生多个JavaVM对象,但是在Android上,每一个Process只有一个art虚拟机对象,也就是在Android进程中是通过有且只有一个虚拟器对象来服务所有Java和C/C++代码 。Java 的dex字节码和C/C++的*.so同时运行ART虚拟机之内,共同使用一个进程空间。之所以可以相互调用,也是因为有ART虚拟机。当Java 代码需要C/C++代码时,在ART虚拟机加载进*.so库时,会先调用JNI_Onload(),此时就会把JAVA VM对象的指针存储于c层jni组件的全局环境中,在Java层调用C层的本地函数时,调用C本地函数的线程必然通过ART虚拟机来调用C层的本地函数,此时,ART虚拟机会为本地的C组件实例化一个JNIEnv指针,该指针指向ART虚拟机的具体的函数列表,当JNI的c组件调用Java层的方法或者属性时,需要通过JNIEnv指针来进行调用。 当本地C/C++想获得当前线程所要使用的JNIEnv时,可以使用ART虚拟机对象的JavaVM* jvm->GetEnv()返回当前线程所在的JNIEnv*。

参考博客:
https://blog.csdn.net/CV_Jason/article/details/80026265
https://www.cnblogs.com/fnlingnzb-learner/p/7366025.html

你可能感兴趣的:(JNI/NDK入门指南)