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来保存获得的指针以便在任意上下文中使用。
在这里插入图片描述

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

JNIEXPORT void JNICALL Java_com_pax_android2native_JniManager_openJni
  (JNIEnv * env, jobject object)
{
	LOGE(TAG, "Java_com_pax_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 <jni.h>头文件。
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_pax_object2struct_JniTransfer
 * Method:    getJavaBeanFromNative
 * Signature: ()Lcom/pax/object2struct/JavaBean;
 */
JNIEXPORT jobject JNICALL Java_com_pax_object2struct_JniTransfer_getJavaBeanFromNative
  (JNIEnv *, jclass);

/*
 * Class:     com_pax_object2struct_JniTransfer
 * Method:    transferJavaBeanToNative
 * Signature: (Lcom/pax/object2struct/JavaBean;)V
 */
JNIEXPORT void JNICALL Java_com_pax_object2struct_JniTransfer_transferJavaBeanToNative
  (JNIEnv *, jclass, jobject);

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


1.查看JNIEnv定义

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

C语言中JNIEnv声明如下

扫描二维码关注公众号,回复: 8610594 查看本文章
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 对象
New<Type>Array 创建类型为Type的数组对象
Get<Type>Field 获取类型为Type的字段
Set<Type>Field 设置类型为Type的字段的值
GetStatic<Type>Field 获取类型为Type的static的字段
SetStatic<Type>Field 设置类型为Type的static的字段的值
Call<Type>Method 调用返回类型为Type的方法
CallStatic<Type>Method 调用返回值类型为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_pax_android2native_JniManager_openJni
  (JNIEnv * env, jobject object)
{
	LOGE(TAG, "Java_com_pax_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

发布了89 篇原创文章 · 获赞 92 · 访问量 31万+

猜你喜欢

转载自blog.csdn.net/tkwxty/article/details/103539392
今日推荐