本文转载自邓凡平的《深入理解Android卷I》第三章深入理解JNI
目录
4.2.2 动态注册,使用JNINativeMethod结构,定义如下:
1. JNI概述
JNI是Java Native Interface (java本地调用) 的缩写,这种技术可以做到以下两点:
- java程序中函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数;
- Native程序中的函数可以调用Java层的函数,也就是说,C/C++程序中可以调用java的函数。
2. 学习JNI的实例:MediaScanner
- 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是Android平台中多媒体系统的重要组成成分,功能是扫描媒体文件,得到歌曲时长、歌曲作者等媒体信息,并将它们存入到媒体数据库中,供其他应用程序使用。
3. Java 层的 MediaScanner
[MediaScanner.java]
public class MediaScanner
{
static {
// 1. 加载对应的JNI库,media_jni是JNI库的名字
//在实际加载动态库的时候会将其拓展为libmedia_jni.so
System.loadLibrary("media_jni");
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函数。
3.1 加载JNI库
Java要调用native函数时必须通过一个位于JNI层的动态库来实现。
通常会在类的static语句中加载,调用System.loadLibrary即可,参数就是动态库的名字"media_jni",无需添加后缀,系统会自动拓展生成,Linux拓展成libmedia_jni.so,Windows拓展成media_jni.dll。
3.2 Java的native函数和总结
从上面代码中可以发现,native_init和processFile函数前都有Java的关键字native,它表示这两个函数将由JNI层来实现。
因此,只需要完成下面两项工作就可以使用JNI了:
- 加载对应的JNI库。
- 声明由关键字native修饰的函数。
4. JNI 层的 MediaScanner
MediaScanner的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);
......
const char *pathstr = env->GetstringUTFChars(path,NULL);
......
if(mimeType){
env->ReleaseStringUTFChars (mimeType,mimeTypestr);
}
}
Java层的native_init函数将如何和JNI层的android_media_MediaScanner_native_init函数对应起来呢?
4.1 注册JNI函数
“注册” 就是将Java层的native函数和JNI层对应的实现函数关联起来,有了这种关联,调用Java层的native函数时,就能顺利转到JNI层对应的函数执行了。
native init函数位于android.media这个包中,它的全路径名应该是android.media.MediaScanner.native_init,而JNI层函数的名字是android_media_MediaScanner_native_linit。
因为在Native语言中,符号“ . ”有着特殊的意义,所以JNI层需要把Java函数名称(包括包名)中的“ . ”换成 “_”。也就是通过这种方式,native_init找到了自己JNI层的android.media.MediaScanner.native_init。(注,Java层函数名中如果有一个”_”的话,转换成JNI后变为”_l”)
JNI函数有两种注册方法,分为静态和动态。
4.1.1 静态方法,使用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_linit
JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile
静态方法是根据函数名来建立Java函数和JNI函数之间的关联关系的,它要求JNI层函数的名字必须遵循特定的格式。
当Java层调用native_init函数时,它会从对应的JNl库中寻找Java_android_media_MediaScanner_native_linit函数,如果设有,就会报错。如果找到,则会为这个native_init和Java_android_media_MediaScanner_native linit建立一个关联关系,其实就是保存JNl层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。
弊端:
- 需要编译所有声明了native函数的Java类,每个所生成的class文件都得用javah生成一个头文件。
- javah生成的JNI层函数名特别长,书写起来很不方便。
- 初次调用native函数时要根据函数名字搜索对应的NI层函数来建立关联关系,这样会影响运行效率。
4.2.2 动态注册,使用JNINativeMethod结构,定义如下:
typedef struct {
//Java中native函数的名字,不用携带包的路径,例如“native_init”
const char* name
//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合
const char* signature;
void* fnPtr;//JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;
MediaScannerJNI是如何使用这个结构体的呢?
[-->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中的哪个类
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaScanner", gMethods, NELEM(gMethods));
}
调用AndroidRuntime的registerNativeMethods函数,用来完成注册工作。
[-->AndroidRunTime.cpp]
int AndroidRuntime::registerNativeMethods(JNIEnv*env,
constchar* className, const JNINativeMethod* gMethods, int numMethods)
{
//调用jniRegisterNativeMethods函数完成注册
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
其中registerNativeMethods函数又会调用jniRegisterNativeMethods函数完成注册,该函数是Android平台中为了方便JNI使用而提供的一个帮助函数。
[-->JNIHelp.c]
int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = (*env)->FindClass (env,className);
......
//实际上是调用JNIEnv的RegisterNatives函数完成注册的
if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
return -1;
}
return 0;
}
简单来说,动态注册使用两个函数就可以完成:
/*
env指向一个JNIEnv结构体,非常重要。classname为对应的Java类名,由于JNINativeMethod中使用的函数名并非全路径名,所以要指明哪个是类。
*/
jclass clazz = (*env)->FindClass(env, className);
//调用JNIEnv的RegisterNatives函数,注册关联关系。
(*env)->RegisterNatives(env, clazz, gMethods, numMethods);
那动态注册的函数在什么时候和什么地方被调用呢?
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫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;
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;//必须返回这个值,否则会报错。
}
接下来是Java和JNI层数据类型的转换。
4.2 数据类型转换
在Java中调用native函数传递的参数是Java数据类型,那么这些参数类型到了JNI层会变成什么呢?
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这二者的。先来看基本数据类型的转换。
4.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引用数据类型的转换。
4.2.2 引用数据类型的转换
Java引用类型 | Native类型 | Java引用类型 | Native类型 |
All objects | jobject | char[] | jcharArray |
java.lang.Class实例 | jclass | short[] | jshortArray |
ava.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, 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)
这段代码,引出了下面的主角——JNIEnv。
4.3 JNIEnv介绍
JNIEnv是一个和线程相关的,代表JNI环境的结构体,如图展示了JNIEnv的内部结构:
从上图可知,JNIEnv实际上就是提供了一些JNI系统函数。通过这些函数可以做到:
- 调用Java的函数
- 操作jobject对象等很多事情
上面提到说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.4 通过JNIEnv操作jobject
前面提到过一个问题,即Java的引用类型除了少数几个外,最终在JNI层都用jobject来表示对象的数据类型,那么该如何操作这个jobject呢?
从另外一个角度来解释这个问题。一个Java对象是由什么组成的?当然是它的成员变量和成员函数了。那么,操作jobject的本质就应当是操作这些对象的成员变量和成员函数。所以应先来看与成员变量及成员函数有关的内容。
4.4.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。
MediaScanner中他们是这样使用的:
[-->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后,接下来就可以使用了。
4.4.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()
4.5 jstring介绍
jstring表示Java中的String类型,虽然jstring是一种独立的数据类型,但是它并没有提供成员函数供操作。要想操作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);
......
}
4.6 JNI类型签名的介绍
先来看动态注册中的一段代码:
tatic JNINativeMethod gMethods[] = {
......
{
"processFile"
//processFile的签名信息,这么长的字符串,是什么意思?
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void*)android_media_MediaScanner_processFile
},
......
}
根据前面的介绍可知,那个很长的字符串是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类型。
函数签名不仅看起来麻烦,写起来更麻烦,稍微写错一个标点就会导致注册失败。所以,在具体编码时,可以定义字符串宏,这样改起来也方便。 下表为常见的类型标识:
类型标示 | 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类型是数组,则标示中会有一个“[”,另外,引用类型(除基本类型的数组外)的标示最后都有一个“;”。
函数签名 | 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,就不用死记硬背上面的类型标示了。
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)如果不调用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或许是进程占用过多的一个原因,请务必注意这一点。
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层代码必须关注的事情,在编写代码时一定要小心对待。