一. NDK开发之JavaVM
JNI定义了两个关键数据结构,一个是JavaVM,一个是JNIEnv,在前面我们看jni.h的时候也看到了,只是没有仔细查看他们的定义,这里先看第一个JavaVM,本质上,JavaVM是一个指向函数表的二级指针(在C++版本中,它们是一些类,这些类具有指向函数表的指针,并具有每个通过该函数表间接调用的JNI函数的成员函数。)JavaVM提供”调用接口”函数,我们可以利用此类的函数创建和销毁JavaVM。理论上,每个进程可以有多个JavaVM,但在Android中只允许有一个。
上面这段话为官方原话,但对于初学者来讲,可能还不是很好理解,所以我们就以官方所描述的内容进行讲解分析。
首先,说JavaVM本质上是一个二级指针,也就是我们说的指针的指针;
首先说指针 :
1 2 3 4 5 概念,它其实就是内存地址,比如说我们有一个变量,存放在某个位置,那么这个位置的地址就是这个位置的指针,指向这个位置。举个最简单 的例子就是,我们一般会在C语言中申请一块空间来存储东西,申请空间一般用malloc,这个函数返回了一个void *类型的返回值,返回的 这个值就是这块空间的地址。 我们拿着这个地址就可以去操作这块空间了。
指针变量 :
1 2 指针变量首先是一个变量,它和我们平时定义的int,char,float,double等等变量没有任何区别,本身都是用来存数据的,只是int, char,float,double等存的是一个值,而指针变量存放的是一个地址,这个地址指向了一块空间。
理解了这两个概念,我们再说几个常见的概念:
1 2 函数指针:指向函数的指针,我们拿着这个指针就可以直接调用这个函数。 结构体指针:指向结构体的指针,我们拿着这个指针就可以操作结构体。
所以,简单理解,我们有指针就可以做相关的操作。
指针和指针的指针(所谓二级指针):
指针,也叫一级指针,它指向的是直接可以操作的内存空间。
指针的指针,也叫二级指针,它指向的是一级指针,一级指针指向的是可以操作的内存空间。如下图,一级指针直接指向可操作的内存空间,指针的指针是指向了指针,然后通过指针再指向可操作的内存空间(图中的102也是一个指针)。
在了解了上面的基本概念之后,补充一下在C中和C++中调用上的一些区别,我们看一下jni.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 *); }; 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 }; #if defined(__cplusplus) typedef _JNIEnv JNIEnv;typedef _JavaVM JavaVM;#else typedef const struct JNINativeInterface * JNIEnv;typedef const struct JNIInvokeInterface * JavaVM;#endif
知道了他们的调用区别之后,再看一下JavaVM到底有什么用,那就要看他的函数了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 };
通过上面的注释可以看到,JavaVM提供了获取JNIEnv的函数,将当前线程附着到java虚拟机中以及分离等功能。
本质上就是和javaVM取得联系,使得我们可以和java端进行通信,因为虚拟机并不知道我们C/C++层的线程,所以他们不能直接通信,所以,需要将线程附着到虚拟机上,这样我们就可以获得虚拟机的环境,从而和Java端通信。
二. NDK开发之JNIEnv
前面了解了JavaVM之后,我们再看一下JNIEnv,了解一下它是干什么的。同样的,看一下它的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 struct _JNIEnv { const struct JNINativeInterface * functions; #if defined(__cplusplus) jstring NewString (const jchar* unicodeChars, jsize len) { return functions->NewString (this , unicodeChars, len); } jsize GetStringLength (jstring string) { return functions->GetStringLength (this , string); } const jchar* GetStringChars (jstring string, jboolean* isCopy) { return functions->GetStringChars (this , string, isCopy); } void ReleaseStringChars (jstring string, const jchar* chars) { functions->ReleaseStringChars (this , string, chars); } jstring NewStringUTF (const char * bytes) { return functions->NewStringUTF (this , bytes); } jsize GetStringUTFLength (jstring string) { return functions->GetStringUTFLength (this , string); } const char * GetStringUTFChars (jstring string, jboolean* isCopy) { return functions->GetStringUTFChars (this , string, isCopy); } void ReleaseStringUTFChars (jstring string, const char * utf) { functions->ReleaseStringUTFChars (this , string, utf); } jsize GetArrayLength (jarray array) { return functions->GetArrayLength (this , array); } jobjectArray NewObjectArray (jsize length, jclass elementClass, jobject initialElement) { return functions->NewObjectArray (this , length, elementClass, initialElement); } jobject GetObjectArrayElement (jobjectArray array, jsize index) { return functions->GetObjectArrayElement (this , array, index); } void SetObjectArrayElement (jobjectArray array, jsize index, jobject value) { functions->SetObjectArrayElement (this , array, index, value); } #endif };
看一下上面的函数,时不是很眼熟,就是前面我们讲过的常用函数的使用,所以JNIEnv的作用就是提供了我们和JavaVM通信的一些接口函数。
另外,它和JavaVM一样,也分别定义了C++和C的版本。原理也是一样的,在C++中通过一个结构体封装了一下,C中直接使用的是指针重命名的。所以调用上也和JavaVM一样。
1 2 3 4 env->xxxxx(); (*env)->xxxxx();
三. 全局引用和局部引用和弱全局引用
局部引用:
引用在C++和java以及一些面向对象的语言中都存在,对于传递给原生方法的每个参数,以及JNI函数返回的几乎每个对象都属于局部引用,这意味着,局部引用在当前线程中的当前原生方法运行期间有效。在原生方法返回后,即使对象本身继续存在,该引用也无效。
这适用于jobject的所有子类,包括jclass、jstring和jarray。 所有我们在函数中直接获取到的对象都是局部的,如果想让它在全局可用,需要将他变成全局引用。
这个规则对于JNIEnv不适用,它属于线程。
全局引用: 局部引用在函数返回后便不能使用了,但如果想让局部引用在函数返回后依旧可以使用,就要将它变成全局的。
将一个局部引用变成全局引用可以通过NewGLobalRef方法,比如我们不想在每个地方都去获取java的class对象,就可以在一次获取后,将他变成全局的。
1 2 3 4 5 6 7 8 jclass localClazz; jclass globalClazz; jclass localClass = env->FindClass ("MyClass" ); jclass globalClass = reinterpret_cast <jclass>(env->NewGlobalRef (localClass)); env->DeleteGlobalRef (globalClass);
以上两种是我们比较常用的,还有一种引用叫弱全局引用
弱全局引用:
它是全局引用的一种类型,与全局引用一样,它也可以在方法返回之后,依旧可以使用,但和全局引用不同的是弱全局引用不会阻止潜在的对象被垃圾回收,所以使用的时候记得检测这个对象是否还有效,避免崩溃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 jclass localClazz; jclass weakGlobalClazz; jclass localClass = env->FindClass ("MyClass" ); jclass weakGlobalClazz = reinterpret_cast <jclass>(env->NewWeakGlobalRef (localClass)); if (JNI_FALSE == env->IsSameObject (weakGlobalClazz, NULL )){ }else { }
四. JNI_OnLoad
最后一个知识点,JNI_OnLoad,这个函数我们还没有使用过,这是一个被系统调用的函数,当库被加载的时候,系统会主动调用它。
那么它有什么用处?
一般情况下,我们在这个函数中获取JavaVM 对象保存到全局。 或者在这里动态注册JNI函数。再或者在这里获取一些类的class缓存起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 JNIEXPORT jint JNI_OnLoad (JavaVM* vm, void * reserved) { JNIEnv* env; if (vm->GetEnv (reinterpret_cast <void **>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } jclass c = env->FindClass ("com/example/app/package/MyClass" ); if (c == nullptr ) return JNI_ERR; static const JNINativeMethod methods[] = { {"nativeFoo" , "()V" , reinterpret_cast <void *>(nativeFoo)}, {"nativeBar" , "(Ljava/lang/String;I)Z" , reinterpret_cast <void *>(nativeBar)}, }; int rc = env->RegisterNatives (c, methods, sizeof (methods)/sizeof (JNINativeMethod)); if (rc != JNI_OK) return rc; return JNI_VERSION_1_6; }
以上就是今天所讲解的全部内容了。这章内容稍微偏理论,我们将在下节课讲解其他内容时用到上面的所有知识点,所以务必掌握。