深入理解JNI

Posted zhchoutai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解JNI相关的知识,希望对你有一定的参考价值。

深入理解JNI

本章主要内容

·? 通过一个实例,介绍JNI技术和在使用中应注意的问题。

本章涉及的源代码文件名称及位置

以下是本章分析的源代码文件名称及其位置。

·? MediaScanner.java

framework/base/media/java/src/android/media/MediaScanner.java

·? android_media_MediaScanner.cpp

framework/base/media/jni/MediaScanner.cpp

·? android_media_MediaPlayer.cpp

framework/base/media/jni/android_media_MediaPlayer.cpp

·? AndroidRunTime.cpp

framework/base/core/jni/AndroidRunTime.cpp

·? JNIHelp.c

dalvik/libnativehelper/JNIHelp.c

2.1? 概述

JNI。是Java Native Interface的缩写,中文为Java本地调用。通俗地说,JNI是一种技术。通过这样的技术能够做到以下两点:

·? Java程序中的函数能够调用Native语言写的函数,Native一般指的是C/C++编写的函数。

·? Native程序中的函数能够调用Java层的函数,也就是在C/C++程序中能够调用Java的函数。

在平台无关的Java中,为什么要创建一个和Native相关的JNI技术呢?这岂不是破坏了Java的平台无关特性吗?本人认为,JNI技术的推出可能是出于以下几个方面的考虑:

·? 承载Java世界的虚拟机是用Native语言写的。而虚拟机又执行在详细平台上,所以虚拟机本身无法做到平台无关。然而。有了JNI技术,就能够对Java层屏蔽详细的虚拟机实现上的差异了。这样,就能实现Java本身的平台无关特性。

事实上Java一直在使用JNI技术,仅仅是我们平时较少用到罢了。

·? 早在Java语言诞生前,非常多程序都是用Native语言写的,它们遍布在软件世界的各个角落。Java出世后,它受到了追捧,并迅速得到发展,但仍无法对软件世界彻底改朝换代。于是才有了折中的办法。既然已经有Native模块实现了相关功能。那么在Java中通过JNI技术直接使用它们就可以了,免得落下反复制造轮子的坏名声。另外,在一些要求效率和速度的场合还是须要Native语言參与的。

在Android平台上,JNI就是一座将Native世界和Java世界间的天堑变成通途的桥。来看图2-1,它展示了Android平台上JNI所处的位置:

技术分享图片

图2-1? Android平台中JNI示意图

由上图可知。JNI将Java世界和Native世界紧密地联系在一起了。在Android平台上尽情使用Java开发的程序猿们不要忘了。假设没有JNI的支持,我们将寸步难行!

注意,尽管JNI层的代码是用Native语言写的。但本书还是把和JNI相关的模块单独归类到JNI层。

俗话说,百闻不如一见,就来见识一下JNI技术吧。

?

2.2 ?通过实例学习JNI

初次接触JNI,感觉最奇妙的就是。Java居然能够调用Native的函数,可它是怎么做到的呢?网上有非常多介绍JNI的资料。因为Android大量使用了JNI技术。本节就将通过源代码中的一处实例。来学习相关的知识。并了解它是怎样调用Native的函数的。

这个样例,是和MediaScanner相关的。

在本书的最后一章,会详细分析它的工作原理。这里先看和JNI相关的部分,如图2-2所看到的:

技术分享图片

图2-2? MediaScanner和它的JNI

将图2-2与图2-1结合来看,能够知道:

·? Java世界相应的是MediaScanner。而这个MediaScanner类有一些函数是须要由Native层实现的。

·? JNI层相应的是libmedia_jni.so。

media_jni是JNI库的名字,当中。下划线前的“media”是Native层库的名字。这里就是libmedia库。

下划线后的”jni“表示它是一个JNI库。

注意。JNI库的名字能够随便取。只是Android平台基本上都採用“lib模块名_jni.so”的命名方式。

·? Native层相应的是libmedia.so。这个库完毕了实际的功能。

·? MediaScanner将通过JNI库libmedia_jni.so和Native的libmedia.so交互。

从上面的分析中还可知道:

·? JNI层必须实现为动态库的形式。这样Java虚拟机才干载入它并调用它的函数。

以下来看MediaScanner。

MediaScanner是Android平台中多媒体系统的重要组成部分。它的功能是扫描媒体文件。得到诸如歌曲时长、歌曲作者等媒体信息,并将它们存入到媒体数据库中,供其它应用程序使用。

2.2.1 ?Java层的MediaScanner分析

来看MediaScanner(简称MS)的源代码,这里将提取出和JNI有关的部分。其代码例如以下所看到的:

[-->MediaScanner.java]

public class MediaScanner

{

static{ static语句

??? /*

①载入相应的JNI库。media_jni是JNI库的名字。实际载入动态库的时候会拓展成

libmedia_jni.so,在Windows平台上将拓展为media_jni.dll。

*/

???????System.loadLibrary("media_jni");

???????native_init();//调用native_init函数

??? }

.......

//非native函数

publicvoid scanDirectories(String[] directories, String volumeName){

? ......

}

?

//②声明一个native函数。native为Java的keyword,表示它将由JNI层完毕。

privatestatic native final void native_init();

??? ......

privatenative void processFile(String path, String mimeType,

?MediaScannerClient client);

??? ......

}

·? 上面代码中列出了两个比較重要的要点:

1. 载入JNI库

前面说过,如Java要调用Native函数。就必须通过一个位于JNI层的动态库才干做到。顾名思义,动态库就是执行时载入的库,那么是什么时候。在什么地方载入这个库呢?

这个问题没有标准答案,原则上是在调用native函数前,不论什么时候、不论什么地方载入都能够。

通行的做法是。在类的static语句中载入,通过调用System.loadLibrary方法就能够了。

这一点。在上面的代码中也见到了,我们以后就按这样的方法编写代码就可以。另外,System.loadLibrary函数的參数是动态库的名字。即media_jni。系统会自己主动依据不同的平台拓展成真实的动态库文件名称,比如在Linux系统上会拓展成libmedia_jni.so。而在Windows平台上则会拓展成media_jni.dll。

攻克了JNI库载入的问题,再来来看第二个关键点。

2.? Java的native函数和总结

从上面代码中能够发现,native_init和processFile函数前都有Java的keywordnative,它表示这两个函数将由JNI层来实现。

Java层的分析到此结束。

JNI技术也非常照应Java程序猿。仅仅要完毕以下两项工作就能够使用JNI了,它们是:

·? 载入相应的JNI库。

·? 声明由keywordnative修饰的函数。

所以对于Java程序猿来说。使用JNI技术真的是太easy了。只是JNI层可没这么轻松。以下来看MS的JNI层分析。

2.2.2 ?JNI层的MediaScanner分析

MS的JNI层代码在android_media_MediaScanner.cpp中。例如以下所看到的:

[-->android_media_MediaScanner.cpp]

//①这个函数是native_init的JNI层实现。

static void?android_media_MediaScanner_native_init(JNIEnv *env)

{

????jclass clazz;

?

??? clazz= env->FindClass("android/media/MediaScanner");

??? ......

???fields.context = env->GetFieldID(clazz, "mNativeContext","I");

......

return;

}

?

//这个函数是processFile的JNI层实现。

static void android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,

jstring path, jstring mimeType, jobject client)

{

??? MediaScanner*mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

??? ......

??? constchar *pathStr = env->GetStringUTFChars(path, NULL);

? ??......

??? if(mimeType) {

???????env->ReleaseStringUTFChars(mimeType, mimeTypeStr);

??? }

}

上面是MS的JNI层代码。不知道读者看了以后是否会产生些疑惑?

我想,最大的疑惑可能是,怎么会知道Java层的native_init函数相应的是JNI层的android_media_MediaScanner_native_init函数呢?以下就来回答这个问题。

1.???注冊JNI函数

正如代码中凝视的那样,native_init函数相应的JNI函数是android_media_MediaScanner_native_init,但是细心的读者可能要问了,你怎么知道native_init函数相应的是这个android_media_MediaScanner_native_init。而不是其它的呢?莫非是依据函数的名字?

大家知道。native_init函数位于android.media这个包中,它的全路径名应该是android.media.MediaScanner.native_init,而JNI层函数的名字是android_media_MediaScanner_native_init。因为在Native语言中,符号“.”有着特殊的意义,所以JNI层须要把“.”换成“_”。

也就是通过这样的方式,native_init找到了自己JNI层的本家兄弟android.media.MediaScanner.native_init。

上面的问题事实上讨论的是JNI函数的注冊问题。“注冊”之意就是将Java层的native函数和JNI层相应的实现函数关联起来。有了这样的关联,调用Java层的native函数时,就能顺利转到JNI层相应的函数执行了。而JNI函数的注冊实际上有两种方法,以下分别做介绍。

(1)静态方法

我们从网上找到的与JNI有的关资料,一般都会介绍怎样使用这样的方法完毕JNI函数的注冊。这样的方法就是依据函数名来找相应的JNI函数。

这样的方法须要Java的工具程序javah參与,总体流程例如以下:

·? 先编写Java代码,然后编译生成.class文件。

·? 使用Java的工具程序javah,如javah–o output packagename.classname ,这样它会生成一个叫output.h的JNI层头文件。

当中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件中。声明了相应的JNI层函数。仅仅要实现里面的函数就可以。

这个头文件的名字一般都会使用packagename_class.h的样式,比如MediaScanner相应的JNI层头文件就是android_media_MediaScanner.h。

以下。来看这样的方式生成的头文件:

[-->android_media_MediaScanner.h::样例文件]

/* DO NOT EDIT THIS FILE - it is machinegenerated */

#include <jni.h>? //必须包括这个头文件。否则编译通只是

/* Header for class android_media_MediaScanner*/

?

#ifndef _Included_android_media_MediaScanner

#define _Included_android_media_MediaScanner

#ifdef __cplusplus

extern "C" {

#endif

...... 略去一部分凝视内容

//processFile的JNI函数

JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile

? ?????????????????(JNIEnv *, jobject, jstring,jstring, jobject);

?

......//略去一部分凝视内容

//native_init相应的JNI函数

JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init

? (JNIEnv*, jclass);

?

#ifdef __cplusplus

}

#endif

#endif

从上面代码中能够发现。native_init和processFile的JNI层函数被声明成:

//Java层函数名中假设有一个”_”的话,转换成JNI后就变成了”_l”。

JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init

JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile

需解释一下,静态方法中native函数是怎样找到相应的JNI函数的。事实上。过程非常easy:

·? 当Java层调用native_init函数时。它会从相应的JNI库Java_android_media_MediaScanner_native_linit。假设没有。就会报错。假设找到,则会为这个native_init和Java_android_media_MediaScanner_native_linit建立一个关联关系。事实上就是保存JNI层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针就能够了,当然这项工作是由虚拟机完毕的。

从这里能够看出。静态方法就是依据函数名来建立Java函数和JNI函数之间的关联关系的,它要求JNI层函数的名字必须遵循特定的格式。

这样的方法也有几个弊端,它们是:

·? 须要编译全部声明了native函数的Java类。每一个生成的class文件都得用javah生成一个头文件。

·? javah生成的JNI层函数名特别长,书写起来非常不方便。

·? 初次调用native函数时要依据函数名字搜索相应的JNI层函数来建立关联关系,这样会影响执行效率。

有什么办法能够克服上面三种弊端吗?依据上面的介绍。Java native函数是通过函数指针来和JNI层函数建立关联关系的。

假设直接让native函数知道JNI层相应函数的函数指针。不就万事大吉了吗?这就是以下要介绍的另外一种方法:动态注冊法。

(2)动态注冊

既然Java native函数数和JNI函数是一一相应的。那么是不是会有一个结构来保存这样的关联关系呢?答案是肯定的。在JNI技术中,用来记录这样的一一相应关系的,是一个叫JNINativeMethod的结构,其定义例如以下:

typedef struct {

?? //Java中native函数的名字,不用携带包的路径。比如“native_init“。

constchar* name; ???

//Java函数的签名信息,用字符串表示,是參数类型和返回值类型的组合。

?? ?const char* signature;

???void*?????? fnPtr; ?//JNI层相应函数的函数指针,注意它是void*类型。

} JNINativeMethod;

应该怎样使用这个结构体呢?来看MediaScanner JNI层是怎样做的,代码例如以下所看到的:

[-->android_media_MediaScanner.cpp]

//定义一个JNINativeMethod数组,其成员就是MS中全部native函数的一一相应关系。

static JNINativeMethod gMethods[] = {

??? ......

{

"processFile" //Java中native函数的函数名。

//processFile的签名信息。签名信息的知识,后面再做介绍。

"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",???

?(void*)android_media_MediaScanner_processFile //JNI层相应函数指针。

},

?......

?

{

"native_init",???????

"()V",?????????????????????

(void *)android_media_MediaScanner_native_init

},

? ......

};

//注冊JNINativeMethod数组

int register_android_media_MediaScanner(JNIEnv*env)

{

?? //调用AndroidRuntime的registerNativeMethods函数,第二个參数表明是Java中的哪个类

??? returnAndroidRuntime::registerNativeMethods(env,

???????????????"android/media/MediaScanner", gMethods, NELEM(gMethods));

}

AndroidRunTime类提供了一个registerNativeMethods函数来完毕注冊工作,以下看registerNativeMethods的实现。代码例如以下:

[-->AndroidRunTime.cpp]

int AndroidRuntime::registerNativeMethods(JNIEnv*env,

??? constchar* className, const JNINativeMethod* gMethods, int numMethods)

{

??? //调用jniRegisterNativeMethods函数完毕注冊

??? returnjniRegisterNativeMethods(env, className, gMethods, numMethods);

}

当中jniRegisterNativeMethods是Android平台中。为了方便JNI使用而提供的一个帮助函数,其代码例如以下所看到的:

[-->JNIHelp.c]

int jniRegisterNativeMethods(JNIEnv* env, constchar* className,

??? ??????????????????????????????constJNINativeMethod* gMethods, int numMethods)

{

??? jclassclazz;

??? clazz= (*env)->FindClass(env, className);

......

//实际上是调用JNIEnv的RegisterNatives函数完毕注冊的

??? if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {

???????return -1;

??? }

??? return0;

}

wow,好像非常麻烦啊!事实上动态注冊的工作。仅仅用两个函数就能完毕。总结例如以下:

/*

env指向一个JNIEnv结构体。它非常重要,后面会讨论它。classname为相应的Java类名,因为

JNINativeMethod中使用的函数名并不是全路径名。所以要指明是哪个类。

*/

jclass clazz = ?(*env)->FindClass(env, className);

//调用JNIEnv的RegisterNatives函数,注冊关联关系。

(*env)->RegisterNatives(env, clazz, gMethods,numMethods);

所以。在自己的JNI层代码中使用这样的方法,就能够完毕动态注冊了。

这里另一个非常棘手的问题:这些动态注冊的函数在什么时候、什么地方被谁调用呢?好了,不卖关子了。直接给出该问题的答案:

·? 当Java层通过System.loadLibrary载入完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,假设有,就调用它,而动态注冊的工作就是在这里完毕的。

所以,假设想使用动态注冊方法,就必须要实现JNI_OnLoad函数,仅仅有在这个函数中,才有机会完毕动态注冊的工作。静态注冊则没有这个要求,可我建议读者也实现这个JNI_OnLoad函数,因为有一些初始化工作是能够在这里做的。

那么。libmedia_jni.so的JNI_OnLoad函数是在哪里实现的呢?因为多媒体系统非常多地方都使用了JNI,所以码农把它放到android_media_MediaPlayer.cpp中了,代码例如以下所看到的:

[-->android_media_MediaPlayer.cpp]

jint JNI_OnLoad(JavaVM* vm, void* reserved)

{

?? //该函数的第一个參数类型为JavaVM,这但是虚拟机在JNI层的代表喔,每一个Java进程仅仅有一个

? //这样的JavaVM

???JNIEnv* env = NULL;

??? jintresult = -1;

?

??? if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {

?????? ??gotobail;

??? }

?? ?...... //动态注冊MediaScanner的JNI函数。

??? if(register_android_media_MediaScanner(env) < 0) {

????????goto bail;

}

......

returnJNI_VERSION_1_4;//必须返回这个值,否则会报错。

}

JNI函数注冊的内容介绍完了。以下来关注JNI技术中其它的几个重要部分。

JNI层代码中一般要包括jni.h这个头文件。Android源代码中提供了一个帮助头文件JNIHelp.h。它内部事实上就包括了jni.h,所以我们在自己的代码中直接包括这个JNIHelp.h就可以。

2. 数据类型转换

通过前面的分析,攻克了JNI函数的注冊问题。以下来研究数据类型转换的问题。

在Java中调用native函数传递的參数是Java数据类型。那么这些參数类型到了JNI层会变成什么呢?

Java数据类型分为基本数据类型和引用数据类型两种。JNI层也是差别对待这二者的。先来看基本数据类型的转换。

(1)基本类型的转换

基本类型的转换非常easy。可用表2-1表示:

表2-1? 基本数据类型转换关系表

Java

Native类型

符号属性

字长

boolean

jboolean

无符号

8位

byte

jbyte

无符号

8位

char

jchar

无符号

16位

short

jshort

有符号

16位

int

jint

有符号

32位

long

jlong

有符号

64位

float

jfloat

有符号

32位

double

jdouble

有符号

64位

上面列出了Java基本数据类型和JNI层数据类型相应的转换关系。非常easy。只是,应务必注意,转换成Native类型后相应数据类型的字长。比如jchar在Native语言中是16位,占两个字节。这和普通的char占一个字节的情况全然不一样。

接下来看Java引用数据类型的转换。

(2)引用数据类型的转换

引用数据类型的转换如表2-2所看到的:

表2-2? Java引用数据类型转换关系表

Java引用类型

Native类型

Java引用类型

Native类型

All objects

jobject

char[]

jcharArray

java.lang.Class实例

jclass

short[]

jshortArray

java.lang.String实例

jstring

int[]

jintArray

Object[]

jobjectArray

long[]

jlongArray

boolean[]

jbooleanArray

float[]

floatArray

byte[]

jbyteArray

double[]

jdoubleArray

java.lang.Throwable实例

jthrowable

?

?

由上表可知:

·? 除了Java中基本数据类型的数组、Class、String和Throwable外,其余全部Java对象的数据类型在JNI中都用jobject表示。

这一点太让人吃惊了!看processFile这个函数:

//Java层processFile有三个參数。

processFile(String path, StringmimeType,MediaScannerClient client);

//JNI层相应的函数,最后三个參数和processFile的參数相应。

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,

jstring path, jstring mimeType, jobject client)

从上面这段代码中能够发现:

·? Java的String类型在JNI层相应为jstring。

·? Java的MediaScannerClient类型在JNI层相应为jobject。

假设对象类型都用jobject表示,就好比是Native层的void*类型一样,对码农来说,是全然透明的。既然是透明的,那该怎样使用和操作它们呢?在回答这个问题之前。再来仔细看看上面那个android_media_MediaScanner_processFile函数。代码例如以下:

/*

Java中的processFile仅仅有三个參数,为什么JNI层相应的函数会有五个參数呢?第一个參数中的JNIEnv是什么?稍后介绍。第二个參数jobject代表Java层的MediaScanner对象,它表示

是在哪个MediaScanner对象上调用的processFile。假设Java层是static函数的话。那么

这个參数将是jclass,表示是在调用哪个Java Class的静态函数。

*/

android_media_MediaScanner_processFile(JNIEnv*env,

jobject thiz,

jstring path, jstring mimeType, jobject client)

上面的代码。引出了以下几节的主角JNIEnv。

3. JNIEnv介绍

JNIEnv是一个和线程相关的,代表JNI环境的结构体。图2-3展示了JNIEnv的内部结构:

技术分享图片

图2-3? JNIEnv内部结构简图

从上图可知。JNIEnv实际上就是提供了一些JNI系统函数。通过这些函数能够做到:

·? 调用Java的函数。

·? 操作jobject对象等非常多事情。

后面小节中将详细介绍怎么使用JNIEnv中的函数。这里,先介绍一个关于JNIEnv的重要知识点。

上面提到说JNIEnv。是一个和线程有关的变量。也就是说。线程A有一个JNIEnv,线程B有一个JNIEnv。因为线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。读者可能会问,JNIEnv不都是native函数转换成JNI层函数后由虚拟机传进来的吗?使用传进来的这个JNIEnv总不会错吧?是的,在这样的情况下使用当然不会出错。

只是当后台线程收到一个网络消息,而又须要由Native层函数主动回调Java层函数时。JNIEnv是从何而来呢?依据前面的介绍可知。我们不能保存另外一个线程的JNIEnv结构体。然后把它放到后台线程中来用。这该怎样是好?

还记得前面介绍的那个JNI_OnLoad函数吗?它的第一个參数是JavaVM。它是虚拟机在JNI层的代表。代码例如以下所看到的:

//全进程仅仅有一个JavaVM对象,所以能够保存,不论什么地方使用都没有问题。

jint JNI_OnLoad(JavaVM* vm, void* reserved)

正如上面代码所说,不论进程中有多少个线程。JavaVM却是独此一份,所以在不论什么地方都能够使用它。

那么。JavaVM和JNIEnv又有什么关系呢?答案例如以下:

·? 调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。

这样就能够在后台线程中回调Java函数了。

·? 另外,后台线程退出前,须要调用JavaVM的DetachCurrentThread函数来释放相应的资源。

再来看JNIEnv的作用。

4. 通过JNIEnv操作jobject

前面提到过一个问题,即Java的引用类型除了少数几个外。终于在JNI层都用jobject来表示对象的数据类型,那么该怎样操作这个jobject呢?

从另外一个角度来解释这个问题。

一个Java对象是由什么组成的?当然是它的成员变量和成员函数了。

那么,操作jobject的本质就应当是操作这些对象的成员变量和成员函数。所以应先来看与成员变量及成员函数有关的内容。

(1)jfieldID 和jmethodID的介绍

我们知道,成员变量和成员函数是由类定义的,它是类的属性,所以在JNI规则中,用jfieldID 和jmethodID 来表示Java类的成员变量和成员函数。它们通过JNIEnv的以下两个函数能够得到:

jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);

jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);

当中。jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息。如前所看到的,成员函数和成员变量都是类的信息,这两个函数的第一个參数都是jclass。

MS中是怎么使用它们的呢?来看代码,例如以下所看到的:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient构造函数]

?MyMediaScannerClient(JNIEnv *env, jobjectclient)......

{

?//先找到android.media.MediaScannerClient类在JNI层中相应的jclass实例。

jclass mediaScannerClientInterface =

env->FindClass("android/media/MediaScannerClient");

?//取出MediaScannerClient类中函数scanFile的jMethodID。

mScanFileMethodID = env->GetMethodID(

mediaScannerClientInterface, "scanFile",

? ?????????????????????????"(Ljava/lang/String;JJ)V");

?//取出MediaScannerClient类中函数handleStringTag的jMethodID。

?mHandleStringTagMethodID = env->GetMethodID(

mediaScannerClientInterface,"handleStringTag",

???????? ????????????????????"(Ljava/lang/String;Ljava/lang/String;)V");

? ......

}

在上面代码中,将scanFile和handleStringTag函数的jmethodID保存为MyMediaScannerClient的成员变量。为什么这里要把它们保存起来呢?这个问题涉及一个事关程序执行效率的知识点:

·? 假设每次操作jobject前都去查询jmethoID或jfieldID的话将会影响程序执行的效率。所以我们在初始化的时候。就能够取出这些ID并保存起来以供兴许使用。

取出jmethodID后。又该怎么用它呢?

(2)使用jfieldID和jmethodID

以下再看一个样例,其代码例如以下所看到的:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile]

?virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

??? {

???????jstring pathStr;

??????? if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

???????

/*

调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的參数:

第一个是代表MediaScannerClient的jobject对象,

第二个參数是函数scanFile的jmethodID。后面是Java中scanFile的參数。

*/

???????mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

?

???????mEnv->DeleteLocalRef(pathStr);

???????return (!mEnv->ExceptionCheck());

}

明确了,通过JNIEnv输出的CallVoidMethod。再把jobject、jMethodID和相应參数传进去,JNI层就能够调用Java对象的函数了。

实际上JNIEnv输出了一系列相似CallVoidMethod的函数。形式例如以下:

NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)。

当中type是相应Java函数的返回值类型,比如CallIntMethod、CallVoidMethod等。

上面是针对非static函数的,假设想调用Java中的static函数,则用JNIEnv输出的CallStatic<Type>Method系列函数。

如今。我们已了解了怎样通过JNIEnv操作jobject的成员函数,那么怎么通过jfieldID操作jobject的成员变量呢?这里,直接给出总体解决方式,例如以下所看到的:

//获得fieldID后。可调用Get<type>Field系列函数获取jobject相应成员变量的值。

NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)

//或者调用Set<type>Field系列函数来设置jobject相应成员变量的值。

void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

//以下我们列出一些參加的Get/Set函数。

GetObjectField()???????? SetObjectField()

GetBooleanField()?? ????? SetBooleanField()

GetByteField()?????????? SetByteField()

GetCharField()?????????? SetCharField()

GetShortField()????????? SetShortField()

GetIntField()??????????? SetIntField()

GetLongField()?????????? SetLongField()

GetFloatField()????????? SetFloatField()

GetDoubleField() ???????????????? SetDoubleField()

通过本节的介绍。相信读者已了解jfieldID和jmethodID的作用,也知道怎样通过JNIEnv的函数来操作jobject了。尽管jobject是透明的,但有了JNIEnv的帮助,还是能轻松操作jobject背后的实际对象了。

5. jstring介绍

Java中的String也是引用类型,只是因为它的使用非常频繁。所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。尽管jstring是一种独立的数据类型,但是它并没有提供成员函数供操作。相比而言,C++中的string类就有自己的成员函数了。

那么该怎么操作jstring呢?还是得依靠JNIEnv提供的帮助。

这里看几个有关jstring的函数:

·? 调用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),能够从Native的字符串得到一个jstring对象。事实上,能够把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说。jstring就是一个Java String。但因为Java String存储的是Unicode字符串,所以NewString函数的參数也必须是Unicode字符串。

·? 调用JNIEnv的NewStringUTF将依据Native的一个UTF-8字符串得到一个jstring对象。

在实际工作中,这个函数用得最多。

·? 上面两个函数将本地字符串转换成了Java的String对象,JNIEnv还提供了GetStringChars和GetStringUTFChars函数,它们能够将Java String对象转换成本地字符串。当中GetStringChars得到一个Unicode字符串,而GetStringUTFChars得到一个UTF-8字符串。

·? 另外,假设在代码中调用了上面几个函数,在做完相关工作后。就都须要调用ReleaseStringChars或ReleaseStringUTFChars函数相应地释放资源,否则会导致JVM内存泄露。这一点和jstring的内部实现有关系,读者写代码时务必注意这个问题。

为了加深印象,来看processFile是怎么做的:

[-->android_media_MediaScanner.cpp]

static void

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path, jstring mimeType, jobject client)

{

???MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz,fields.context);

......

//调用JNIEnv的GetStringUTFChars得到本地字符串pathStr

??? constchar *pathStr = env->GetStringUTFChars(path, NULL);

......

//使用完后,必须调用ReleaseStringUTFChars释放资源

???env->ReleaseStringUTFChars(path, pathStr);

??? ......

}

6. JNI类型签名的介绍

先来看动态注冊中的一段代码:

tatic JNINativeMethod gMethods[] = {

??? ......

{

"processFile"

//processFile的签名信息,这么长的字符串。是什么意思?

"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",???

?(void*)android_media_MediaScanner_processFile

},

? ......

}

上面代码中的JNINativeMethod已经见过了,只是当中那个非常长的字符串"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V"是什么意思呢?

依据前面的介绍可知,它是Java中相应函数的签名信息。由參数类型和返回值类型共同组成。只是为什么须要这个签名信息呢?

·? 这个问题的答案比較简单。因为Java支持函数重载,也就是说,能够定义同名但不同參数的函数。但仅仅依据函数名。是没法找到详细函数的。

为了解决问题,JNI技术中就使用了參数类型和返回值类型的组合。作为一个函数的签名信息,有了签名信息和函数名,就能非常顺利地找到Java中的函数了。

JNI规范定义的函数签名信息看起来非常别扭,只是习惯就好了。它的格式是:

(參数1类型标示參数2类型标示...參数n类型标示)返回值类型标示。

来看processFile的样例:

Java中函数定义为void processFile(String path, String mimeType)

相应的JNI函数签名就是

(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V

?当中。括号内是參数类型的标示,最右边是返回值类型的标示,void类型相应的标示是V。

?当參数的类型是引用类型时,其格式是”L包名;”,当中包名中的”.”换成”/”。

上面样例中的

Ljava/lang/String;表示是一个Java String类型。

函数签名不仅看起来麻烦。写起来更麻烦。略微写错一个标点就会导致注冊失败。

所以,在详细编码时,读者能够定义字符串宏,这样改起来也方便。

表2-3是常见的类型标示:

表2-3? 类型标示示意表

类型标示

Java类型

类型标示

Java类型

Z

boolean

F

float

B

byte

D

double

C

char

L/java/langaugeString;

String

S

short

[I

int[]

I

int

[L/java/lang/object;

Object[]

J

long

?

?

上面列出了一些经常使用的类型标示。请读者注意,假设Java类型是数组,则标示中会有一个“[”,另外,引用类型(除基本类型的数组外)的标示最后都有一个“;”。

再来看一个小样例,如表2-4所看到的:

表2-4? 函数签名小样例

函数签名

Java函数

“()Ljava/lang/String;”

String f()

“(ILjava/lang/Class;)J”

long f(int i, Class c)

“([B)V”

void f(byte[] bytes)

请读者结合表2-3和表2-4左栏的内容写出相应的Java函数。

尽管函数签名信息非常easy写错。但Java提供一个叫javap的工具能帮助生成函数或变量的签名信息,它的用法例如以下:

javap –s -p xxx。当中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印全部函数和成员的签名信息,而默认仅仅会打印public成员和函数的签名信息。

有了javap。就不用死记硬背上面的类型标示了。

7. 垃圾回收

我们知道,Java中创建的对象最后是由垃圾回收器来回收和释放内存的,可它对JNI有什么影响呢?以下看一个样例:

[-->垃圾回收样例]

static jobject save_thiz = NULL; //定义一个全局的jobject

static void

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path,

?jstringmimeType, jobject client)

{

? ......

? //保存Java层传入的jobject对象。代表MediaScanner对象

save_thiz = thiz;

......

return;

}

//假设在某个时间,有地方调用callMediaScanner函数

void callMediaScanner()

{

? //在这个函数中操作save_thiz,会有问题吗?

}

上面的做法肯定会有问题,因为和save_thiz相应的Java层中的MediaScanner非常有可能已经被垃圾回收了,也就是说,save_thiz保存的这个jobject可能是一个野指针,如使用它,后果会非常严重。

可能有人要问,将一个引用类型进行赋值操作,它的引用计数不会添加吗?而垃圾回收机制仅仅会保证那些没有被引用的对象才会被清理。问得对。但假设在JNI层使用以下这样的语句。是不会添加引用计数的。

save_thiz = thiz; //这样的赋值不会添加jobject的引用计数。

那该怎么办?不必操心。JNI规范已非常好地攻克了这一问题,JNI技术一共提供了三种类型的引用。它们各自是:

·? Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject、在JNI层函数中创建的jobject。LocalReference最大的特点就是,一旦JNI层函数返回。这些jobject就可能被垃圾回收。

·? Global Reference:全局引用,这样的对象如不主动释放。就永远不会被垃圾回收。

·? Weak Global Reference:弱全局引用,一种特殊的GlobalReference。在执行过程中可能会被垃圾回收。所以在程序中使用它之前,须要调用JNIEnv的IsSameObject推断它是不是被回收了。

平时用得最多的是Local Reference和Global Reference,以下看一个实例。代码例如以下所看到的:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient构造函数]

?MyMediaScannerClient(JNIEnv *env, jobjectclient)

???????:?? mEnv(env),

? ??????//调用NewGlobalRef创建一个GlobalReference,这样mClient就不用操心被回收了。

???????????mClient(env->NewGlobalRef(client)),

???????????mScanFileMethodID(0),

???????????mHandleStringTagMethodID(0),

???????????mSetMimeTypeMethodID(0)

{

? ......

}

//析构函数

virtual ~MyMediaScannerClient()

{

??mEnv->DeleteGlobalRef(mClient);//调用DeleteGlobalRef释放这个全局引用。

?}

每当JNI层想要保存Java层中的某个对象时。就能够使用Global Reference,使用完后记住释放它就能够了。这一点非常easy理解。以下要讲有关LocalReference的一个问题,还是先看实例。代码例如以下所看到的:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile]

?virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

{

?? jstringpathStr;

?? //调用NewStringUTF创建一个jstring对象,它是Local Reference类型。

?? if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

??????? //调用Java的scanFile函数,把这个jstring传进去

???????mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

???? /*

???? ?依据LocalReference的说明。这个函数返回后,pathStr对象就会被回收。所以

????? 以下这个DeleteLocalRef调用看起来是多余的,事实上不然。这里解释一下原因:

1)假设不调用DeleteLocalRef,pathStr将在函数返回后被回收。

2)假设调用DeleteLocalRef的话。pathStr会马上被回收。这两者看起来没什么差别,

只是代码要是像以下这样的话,虚拟机的内存就会被非常快被耗尽:

????? for(inti = 0; i < 100; i++)

????? {

???????????jstring pathStr = mEnv->NewStringUTF(path);

???????????......//做一些操作

??????????//mEnv->DeleteLocalRef(pathStr); //不马上释放Local Reference

}

假设在上面代码的循环中不调用DeleteLocalRef的话,则会创建100个jstring。

那么内存的耗费就非常可观了。

??? ?*/

?? mEnv->DeleteLocalRef(pathStr);

?? return(!mEnv->ExceptionCheck());

}

所以,没有及时回收的Local Reference也许是进程占用过多的一个原因,请务必注意这一点。

8. JNI中的异常处理

JNI中也有异常,只是它和C++、Java的异常不太一样。当调用JNIEnv的某些函数出错后,会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。

尽管在JNI层中产生的异常不会中断本地函数的执行,但一旦产生异常后,就仅仅能做一些资源清理工作了(比如释放全局引用。或者ReleaseStringChars)。假设这时调用除上面所说函数之外的其它JNIEnv函数。则会导致程序死掉。

来看一个和异常处理有关的样例,代码例如以下所看到的:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile函数]

?virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

?{

???????jstring pathStr;

???????//NewStringUTF调用失败后,直接返回。不能再干别的事情了。

??????? if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

???????......

}

JNI层函数能够在代码中截获和改动这些异常,JNIEnv提供了三个函数进行帮助:

·? ExceptionOccured函数,用来推断是否发生异常。

·? ExceptionClear函数。用来清理当前JNI层中发生的异常。

·? ThrowNew函数。用来向Java层抛出异常。

异常处理是JNI层代码必须关注的事情。读者在编写代码时务小心对待。

2.3 ?本章小结

本章通过一个实例介绍了JNI技术中的几个重要方面。包括:

·? JNI函数注冊的方法。

·? Java和JNI层数据类型的转换。

·? JNIEnv和jstring的用法,以及JNI中的类型签名。

·? 最后介绍了垃圾回收在JNI层中的使用,以及异常处理方面的知识。

相信掌握了上面的知识后,我们会对JNI技术有一个比較清晰的认识。

这里,还要建议读者再认真阅读一下JDK文档中的《Java Native Interface Specification》,它完整和仔细地阐述了JNI技术的各个方面,堪称深入学习JNI的权威指南。

以上是关于深入理解JNI的主要内容,如果未能解决你的问题,请参考以下文章

Android-深入理解zygote

安卓深入理解系统详解(35)

安卓深入理解系统详解(35)

深入理解java虚拟机垃圾收集器与内存分配策略

从本地方法栈看到jni调用

Android Binder原理系统服务的注册过程