在前面的章节JNI数据类型,描述符详解中,我们详解了JNI数据类型和描述符的一些概念,那么在今天我们将要熟悉掌握JNI的开发中另外两个关键知识点JavaVM和JniEnv。
JavaVM,英文全称是Java virtual machine,用咋中国话来说就是Java虚拟机。一个JVM中只有一个JavaVM对象,这个JavaVM则可以在进程中的各线程间共享的,这个特性在JNI开发中是非常重要的。
在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_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;
通过前面的章节我们对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,英文全称是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中的数据结构,详情如下图:
通过上面的图示,我们应该更加了解JNIEnv只在当前线程中有效。本地方法不 能将JNIEnv从一个线程传递到另一个线程中。相同的 Java 线程中对本地方法多次调用时,传递给该本地方法的JNIEnv是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 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++中的不同。
通过分析前面章节C/C++中JNIEnv结构体的话,不难发现,这个结构体当中包含了几乎有所的JNI函数,大致可以分为如下几类:
函数名 | 功能 |
---|---|
FindClass | 该函数用于加载本地定义的类 |
GetObjectClass | 通过对象获取这个类 |
NewGlobalRef | 创建 obj 参数所引用对象的新全局引用 |
NewObject | 构造新 Java 对象 |
NewString | 利用 Unicode 字符数组构造新的 java.lang.String 对象 |
New |
创建类型为Type的数组对象 |
Get |
获取类型为Type的字段 |
Set |
设置类型为Type的字段的值 |
GetStatic |
获取类型为Type的static的字段 |
SetStatic |
设置类型为Type的static的字段的值 |
Call |
调用返回类型为Type的方法 |
CallStatic |
调用返回值类型为Type的static方法 |
常见的JNI函数还有一些,这里由于篇幅问题就不过多介绍了。这里推荐一个博客JNI学习积累之一 ---- 常用函数大全里面有比较详细的描述了,大家可以仔细阅读。当然最好的办法,就是实际使用中慢慢品尝了。
如果是在同一个线程中需要使用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里,每一个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