自己怎么建设网站,用html制作的旅游网页,wordpress采集电影资源,六安建设网站第2章 深入理解JNI 2.1 JNI概述 JNI是Java Native Interface的缩写#xff0c;中文译为Java本地调用。通俗地说#xff0c;JNI是一种技术#xff0c;通过这种技术可以做到以下两点#xff1a;
Java程序中的函数可以调用Native语言写的函数#xff0c;Native一… 第2章 深入理解JNI
2.1 JNI概述 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的支持我们将寸步难行
2.2 学习JNI的实例MediaScanner
初次接触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交互。
2.3 JAVA层的MediaScanner分析
来看MediaScanner简称MS的源码这里将提取出和JNI有关的部分其代码如下所示 MediaScanner.java public class MediaScanner
{static{ //static语句/*1.加载对应的JNI库media_jni是JNI库的名字。实际加载动态库的时候会拓展成libmedia_jni.so在Windows平台上将拓展为media_jni.dll。*/System.loadLibrary(media_jni);native_init();//调用native_init函数}.......//非native函数public void scanDirectories(String[] directories, String volumeName){......}//2.声明一个native函数。native为Java的关键字表示它将由JNI层完成。private static native final void native_init();......private native void processFile(String path, String mimeType,MediaScannerClient client);......
}上面代码中列出了两个比较重要的要点一是加载JNI库另一个是Java的native函数
2.3.1 加载JNI库
前面说过如Java要调用Native函数就必须通过一个位于JNI层的动态库才能做到。顾名思义动态库就是运行时加载的库那么是什么时候在什么地方加载这个库呢
这个问题没有标准答案原则上是在调用native函数前任何时候、任何地方加载都可以。通行的做法是在类的static语句中加载通过调用System.loadLibrary方法就可以了。这一点在上面的代码中也见到了我们以后就按这种方法编写代码即可。另外System.loadLibrary函数的参数是动态库的名字即media_jni。系统会自动根据不同的平台拓展成真实的动态库文件名例如在Linux系统上会拓展成libmedia_jni.so而在Windows平台上则会拓展成media_jni.dll。
解决了JNI库加载的问题再来来看第二个关键点。
2.3.2 Java的native函数和总结
从上面代码中可以发现native_init和processFile函数前都有Java的关键字native它表示这两个函数将由JNI层来实现。
Java层的分析到此结束。JNI技术也很照顾Java程序员只要完成下面两项工作就可以使用JNI了它们是 加载对应的JNI库。 声明由关键字native修饰的函数。
所以对于Java程序员来说使用JNI技术真的是太容易了。不过JNI层可没这么轻松下面来看MS的JNI层分析。
2.4 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函数呢下面就来回答这个问题。
2.4.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函数的注册实际上有两种方法下面分别做介绍。
静态方法
我们从网上找到的与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_linit(JNIEnv*, jclass);#ifdef __cplusplus
}
#endif
#endif从上面代码中可以发现native_init和processFile的JNI层函数被声明成
//Java层函数名中如果有一个”_”的话转换成JNI后就变成了”_l”。
JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_linit
JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile需解释一下静态方法中native函数是如何找到对应的JNI函数的。其实过程非常简单
· 当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层对应函数的函数指针不就万事大吉了吗这就是下面要介绍的第二种方法动态注册法。
动态注册
既然Java native函数数和JNI函数是一一对应的那么是不是会有一个结构来保存这种关联关系呢答案是肯定的。在JNI技术中用来记录这种一一对应关系的是一个叫JNINativeMethod的结构其定义如下
typedef struct {//Java中native函数的名字不用携带包的路径。例如“native_init“。const char* name; //Java函数的签名信息用字符串表示是参数类型和返回值类型的组合。const char* signature;void* fnPtr; //JNI层对应函数的函数指针注意它是void*类型。
} JNINativeMethod;应该如何使用这个结构体呢来看MediaScanner JNI层是如何做的代码如下所示 android_media_MediaScanner.cpp //定义一个JNINativeMethod数组其成员就是MS中所有native函数的一一对应关系。
static JNINativeMethod gMethods[] {......
{
//Java中native函数的函数名。
processFile
//processFile的签名信息签名信息的知识后面再做介绍。
(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V,
//JNI层对应函数指针。
(void*)android_media_MediaScanner_processFile
},......{
native_init,
()V,
(void *)android_media_MediaScanner_native_init
},......
};
//注册JNINativeMethod数组
int register_android_media_MediaScanner(JNIEnv*env)
{//调用AndroidRuntime的registerNativeMethods函数第二个参数表明是Java中的哪个类return AndroidRuntime::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函数完成注册return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}其中jniRegisterNativeMethods是Android平台中为了方便JNI使用而提供的一个帮助函数其代码如下所示 JNIHelp.c int jniRegisterNativeMethods(JNIEnv* env, constchar* className,constJNINativeMethod* gMethods, int numMethods)
{jclass clazz;clazz (*env)-FindClass(env, className);......//实际上是调用JNIEnv的RegisterNatives函数完成注册的if ((*env)-RegisterNatives(env, clazz, gMethods, numMethods) 0) {return -1;}return 0;
}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进程//只有一个这样的JavaVMJNIEnv* env NULL;jint result -1;if(vm-GetEnv((void**) env, JNI_VERSION_1_4) ! JNI_OK) {goto bail;}...... //动态注册MediaScanner的JNI函数。if(register_android_media_MediaScanner(env) 0) {goto bail;}......return JNI_VERSION_1_4;//必须返回这个值否则会报错。
}JNI函数注册的内容介绍完了。下面来关注JNI技术中其他的几个重要部分。 JNI层代码中一般要包含jni.h这个头文件。Android源码中提供了一个帮助头文件JNIHelp.h它内部其实就包含了jni.h所以我们在自己的代码中直接包含这个JNIHelp.h即可。 2.4.2 数据类型转换
通过前面的分析解决了JNI函数的注册问题。下面来研究数据类型转换的问题。
在Java中调用native函数传递的参数是Java数据类型那么这些参数类型到了JNI层会变成什么呢
Java数据类型分为基本数据类型和引用数据类型两种JNI层也是区别对待这二者的。先来看基本数据类型的转换。
基本类型的转换
基本类型的转换很简单可用表2-1表示
表2-1 基本数据类型的转换关系表 JavaNative类型符号属性字长booleanjboolean无符号8位bytejbyte无符号8位charjchar无符号16位shortjshort有符号16位intjint有符号32位longjlong有符号64位floatjfloat有符号32位doublejdouble有符号64位
上面列出了Java基本数据类型和JNI层数据类型对应的转换关系非常简单。不过应务必注意转换成Native类型后对应数据类型的字长例如jchar在Native语言中是16位占两个字节这和普通的char占一个字节的情况完全不一样。
接下来看Java引用数据类型的转换。
引用数据类型的转换
引用数据类型的转换如表2-2所示
表2-2 Java引用数据类型转换关系表 Java引用类型Native类型Java引用类型Native类型All objectjobjectchar[]jcharArrayjava.lang.Class实例jclassshort[]jshortArrayjava.lang.String实例jstringint[]jintArrayObject[]jobjectArraylong[]jlongArrayboolean[]jbooleanArrayfloat[]jfloatArraybyte[]jbyteArraydouble[]jdoubleArrayjava.lang.Throwable实例jthrowable
由上表可知
· 除了Java中基本数据类型的数组、Class、String和Throwable外其余所有Java对象的数据类型在JNI中都用jobject表示。
这一点太让人惊讶了看processFile这个函数
//Java层processFile有三个参数。
processFile(String path, String mimeType, 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)2.4.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的作用。
2.4.4 通过JNIEnv操作jobject
前面提到过一个问题即Java的引用类型除了少数几个外最终在JNI层都用jobject来表示对象的数据类型那么该如何操作这个jobject呢
从另外一个角度来解释这个问题。一个Java对象是由什么组成的当然是它的成员变量和成员函数了。那么操作jobject的本质就应当是操作这些对象的成员变量和成员函数。所以应先来看与成员变量及成员函数有关的内容。
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后又该怎么用它呢
使用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 CalltypeMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...)。其中type是对应Java函数的返回值类型例如CallIntMethod、CallVoidMethod等。
上面是针对非static函数的如果想调用Java中的static函数则用JNIEnv输出的CallStaticMethod系列函数。
现在我们已了解了如何通过JNIEnv操作jobject的成员函数那么怎么通过jfieldID操作jobject的成员变量呢这里直接给出整体解决方案如下所示
//获得fieldID后可调用GettypeField系列函数获取jobject对应成员变量的值。
NativeType GettypeField(JNIEnv *env,jobject obj,jfieldID fieldID)
//或者调用SettypeField系列函数来设置jobject对应成员变量的值。
void SettypeField(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背后的实际对象了。
2.4.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得到本地字符串pathStrconstchar *pathStr env-GetStringUTFChars(path, NULL);......//使用完后必须调用ReleaseStringUTFChars释放资源env-ReleaseStringUTFChars(path, pathStr);......
}2.4.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类型ZbooleanFfloatBbyteDdoubleCcharL/java/langaugeString;StringSshort[Iint[]Iint[L/java/lang/object;Object[]Jlong
上面列出了一些常用的类型标示。请读者注意如果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)
虽然函数签名信息很容易写错但Java提供一个叫javap的工具能帮助生成函数或变量的签名信息它的用法如下
javap -s -p xxx其中xxx为编译后的class文件s表示输出内部数据类型的签名信息p表示打印所有函数和成员的签名信息而默认只会打印public成员和函数的签名信息。
有了javap就不用死记硬背上面的类型标示了。
2.4.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使用完后记住释放它就可以了。这一点很容易理解。下面要讲有关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如果不调用DeleteLocalRefpathStr将在函数返回后被回收。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或许是进程占用过多的一个原因请务必注意这一点。
2.4.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.5 小结
本章通过一个实例介绍了JNI技术中的几个重要方面包括
JNI函数注册的方法。Java和JNI层数据类型的转换。JNIEnv和jstring的使用方法以及JNI中的类型签名。最后介绍了垃圾回收在JNI层中的使用以及异常处理方面的知识。
相信掌握了上面的知识后我们会对JNI技术有一个比较清晰的认识。这里还要建议读者再认真阅读一下JDK文档中的《Java Native Interface Specification》它完整和细致地阐述了JNI技术的各个方面堪称深入学习JNI的权威指南。