摘录、参考文档:
1.深入理解Android:卷1 作者:邓凡平
上一章,讲了关于JNI注册的相关知识;
这一章讲的内容比较多,主要是以下几方面的内容:
1)java与JNI之间的数据类型转换;
2)JNIEnv的介绍;
3)JNIEnv的使用,如何操作jobject;
4)jstring介绍。
1. 数据类型转换
在Java中调用native函数传递的参数是java数据类型,那么这些参数到了JNI层会变成什么呢?
java数据类型分为基本数据类型和应用数据类型两种,JNI层也是区别对待两者的。先来看看基本数据类型的转换。
1.基本数据类型转换
上面列出了java的基本数据类型和jni层数据类型对应的转换关系,非常简单。不过,务必注意转换成native类型后对应数据类型的字长。
2.应用数据类型的转换
由上表可知:除了java中基本数据类型的数组、Class、String和Throwable外,其余所有java对象的数据类型在JNI中都用jobject表示。
可以看看processFile这个函数:
从上面这段代码中可以发现:
1)java中的String类型对应jni中的jstring类型;
2)java中的MediaScannerClient对象类型在JNI层对应jobject类型。
如果对象类型都用jobject表示,就好比是native层的void*类型一样,对于我们来说,它们完全是透明的。既然是透明的,那么我们如何来操作它们呢?在回答这个问题之前,我们在仔细看一下上面的android_media_MediaScanner_processFile函数,代码如下:
该如何操作jobject,使用JNIEnv。JNIEnv是下面将要说的重点。
2.JNIEnv介绍
JNIEnv是一个与线程相关的代表JNI环境的结构体。从上图可知,JNIEnv实际上就是提供了一些JNI系统函数。通过这些函数可以:
1)调用java函数;
2)操作jobject对象做很多事情。
JNIEnv是一个与线程相关的对象。也就是说,线程A有一个JNIEnv,线程B也有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。但是,JNIEnv不都是native函数转换成jni函数后由虚拟机传进来的吗?使用传进来的这个JNIEnv总不会错吧?是的,在这种情况下使用当然不会报错。只是当后台线程收到一个网络消息,而又需要由native层函数主动回调java层函数时,JNIEnv从何而来?根据前面的介绍可知,我们不能保存另外一个线程的JNIEnv结构体,然后把它放到后台线程中用。这该如何是好?
还记得上一篇文章介绍动态注册JNI函数时的那个JNI_OnLoad函数吗?它的第一个参数是JavaVM,它是虚拟机在JNI层的代表,全进程只有一个JavaVM对象,不论进程中有多少个线程,JavaVM却是独此一份,所以可以保存,并且在任何地方使用都没有问题。
那么,JavaVM和JNIEnv又有什么关系呢?答案如下:
1)调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调java函数了;
2)另外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。
对于JNIEnv的介绍到此为止,下面我们来说说JNIEnv的使用。
3.通过JNIEnv操作jobject
上面说到过一个问题,即Java的应用类型除了少数的几个外,最终在JNI层都会用jobject来表示对象的数据类型,那么该如何操作这个jobject呢?
从另外一个角度解释这个问题。一个java对象是由什么组成的?当然是它的成员变量和成员函数了。那么,操作jobject的本质就应当是操作这些对象的成员变量和成员函数。所以应先来看与成员变量及成员函数有关的内容。
1)jfieldID和jmethodID介绍
我们知道,成员变量和成员函数都是由类定义的,它们是类的属性,所以在JNI规则中,用jfieldID和jmethodID来表示java类的成员变量和成员函数,可通过JNIEnv的下面两个函数得到:
其中,jclass代表java类,name表示成员变量或成员函数的名字,sig为这个函数或变量的签名信息。如上图所示,成员变量和成员函数都是类的信息,这两个函数的第一个参数都是jclass。
我们来看看在MediaScanner中是怎么使用它们的:
在上面的代码中,讲scanFile和handleStringTag函数的jmethodID保存为MyMediaScannerClient的成员变量。为什么要保存它们?这个问题涉及一个关于程序运行效率的知识点:
如果每次操作jobject前都去查询jfieldID和jmethodID,那么将会影响程序运行的效率,所以我们在初始化的时候可以取出这些ID并保存起来以供后续使用。
取出jmethod之后,又该如何使用它?
2.使用jfieldID和jmethodID
再看一个例子:
明白了吧,通过JNIEnv输出CallVoidMethod,再把jobject、jmethodID和对应的参数传进去,JNI层就能够调用java对象的函数了。
实际上JNIEnv输出了一系列类似CallVoidMethod的函数,形式如下:
NativeType Call
其中type对应java函数的返回值类型,例如CallIntMethod、CallVoidMethod等。
上面是针对非static函数的,如果想调用java中的static函数,则用JNIEnv提供的CallStatic
现在我们知道了如何通过JNIEnv操作jobject的成员函数,那么如何通过JNIEnv操作jobject的成员变量呢?如下所示:
现在,我们已经了解了jfieldID和jmethodID的作用,也知道了如何通过JNIEnv的函数来操作jobject。虽然jobject是透明的,但有了JNIEnv的帮助,还是能轻松操作jobject背后的实际对象的。
4. jstring介绍
Java中的String也是应用类型,不过由于它的使用频率较高,所以在JNI规范中单独创建了一个jstring类型来表示java中的String类型。虽然jstring是一种独立的数据类型,但它并没有提供成员函数以便操作。而c++中的string类是有自己的成员函数的。那么该怎么操作jstring呢?还是得依靠JNIEnv提供帮助。这里看几个有关jstring的函数:
1)调用JNIEnv的NewString(JNIEnv *env, const jchar *unicodeChars, jsize len),可以从native的字符串得到一个jstring对象。其实,可以把一个jstring对象看成是java中String对象在JNI层的代表,也就是说,jstring就是一个java String。但是由于java String 存储的是Unicode字符串,所以NewString函数的参数也必须是Unicode字符串。
2)调用JNIEnv的NewStringUTF函数,将根据native的一个UTF-8字符串得到一个jstring对象,在实际工作中,这个函数用的最多。
3)上面两个函数将本地字符串转换成了java的String对象,JNIEnv还提供了GetStringChars函数和GetStringUTFChars函数,它们可以将java的String对象转换成本地字符串。其中GetStringChars得到一个Unicode字符串,而GetStringUTFChars得到一个UTF-8字符串。
4)另外,如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用ReleaseStringChars和ReleaseStringUTFChars函数来对应的释放资源,否则会导致JVM泄露。这一点和jstring的内部实现有关,读者写代码时务必注意这个问题。
为了加深印象,来看processFile是怎么做的。
文章到这里就结束了。
在这里,感谢深入理解Android的作者邓凡平,谢谢他的贡献。