本人正在学习邓平凡所写的《深入理解Android 卷Ⅰ》,在此做下笔录。
JNI是Java Native Interface 的缩写,中文为“Java 本地调用”。通俗地说,JNI 是一种技术,通过这种技术可以做到以下两点:
- Java 程序中的函数可以调用Native 语言写的函数,Native 一般指C/C++ 编写的函数;
- Native程序中的函数可以调用Java 层的函数,也就是说在C/C++ 程序中可以调用Java 的函数。
也就是说JNI技术就是连接Native 世界和Java 世界的桥梁。如下图所示,展示了Android 平台上JNI 所处的位置:
下图展示了MediaScanner与JNI相关的部分:
- 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.so 和Native 层的libmedia.so 交互。
先来看MediaScanner(简称MS)的源码,这里将提取出与JNI 有关的部分,其代码如下所示
public class MediaScanner
{
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 函数。
这个问题没有标准答案。
原则上是,在调用native 函数前,任何时候、任何地方加载都可以。通行的做法是在类的static 语句中加载,调用
System.LoadLibrary
方法就可以了。这一点在上面的代码中也看到。另外,System.loadLibrary
函数的参数是动态库的名字,即media_jni。系统会自动根据不同的平台拓展成真实的动态库文件名,例如在Linux 系统上会拓展成libmedia_jni.so,而在Windows 平台上则会拓展成media_jni.dll。
MS的JNI层代码在android_media_MediaScanner.cpp 中,如下所示:
// 1. 这个函数是native_init 的JNI 层实现。
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
ALOGV("native_init");
jclass clazz = env->FindClass(kClassMediaScanner);
if (clazz == NULL) {
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
if (fields.context == NULL) {
return;
}
}
// 2. 这个函数是processFile 的JNI 层实现。
static jboolean
android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path,
jstring mimeType, jobject client)
{
ALOGV("processFile");
// Lock already hold by processDirectory
MediaScanner *mp = getNativeScanner_l(env, thiz);
......
const char *pathStr = env->GetStringUTFChars(path, NULL);
......
if (mimeType) {
env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
}
return result != MEDIA_SCAN_RESULT_ERROR;
}
对于上面的代码,要如何才能知道Java 层的native_init 函数对应的是JNI 层的android_media_MediaScanner_native_init
函数呢?下面就来回答这个问题。
native_init
函数位于android.media 这个包中,它的全路径名应该是android.media.MediaScanner.native_init
,而JNI 层函数的名字是android_media_MediaScanner_native_init
。因为在Native 语言中,符号“.”换成了“_”。也就是通过这种方式,native_init
找到了自己JNI 层的本家兄弟android.media.MediaScanner.native_init
。
“注册”之意就是将Java 层的native 函数和JNI 层对应的实现函数关联起来,有了这种关联,调用Java 层的native 函数时,就能顺利赚到JNI 层对应的函数执行了。
而JNI 函数的注册方法实际上有两种,下面分别做介绍。
整体流程如下:
- 先编写Java 代码,然后编译生产.class 文件。
- 使用Java 的工具程序Javah,如
Javah -o ouput packagename.classname
,这样它会生成一个叫output.h 的JNI 层头文件。其中packagename.classname 是Java 代码编译后的class 文件,而在生成的ouput.h 文件里,声明了对应的JNI 层函数,只要实现里面的函数即可。
这个头文件的名字一般会使用packagename_class.h 的样式,例如MediaScanner 对应的JNI 层头文件就是android_media_MediaScanner.h。
静态方法是根据函数名来建立Java 函数和JNI 函数之间的关联关系的,而且它要求JNI 层函数的名字必须遵循特定的格式。这种方法也有几个弊端,即:
- 需要编译所有声明了native 函数的Java 类,每个所生成的class 文件都得用javah 生成一个头文件。
- javah 生成的JNI 层函数名特别长,书写起来很不方便。
- 初次调用native 函数时要根据函数名字搜索对应的JNI 层函数来建立关联关系,这样会影响运行效率。
根据上面的介绍可知,Java native 函数是通过函数指针来和JNI 层函数建立关联关系的。如果直接让native 函数知道JNI 层对应函数的函数指针是否可以完成注册?下面介绍第二种方法:动态注册法。
在JNI 技术中,用来记录Java native 函数和JNI 函数这种一一对应关系的,是一个叫JNINativeMethod 的结构,其定义如下:
typedef struct {
// Java 中Native 函数的名字,不用携带包的路径,例如"native_init".
count char* name;
// Java 函数的签名信息,用字符串表示,是参数类型和返回值类型的组合.
const char* signature;
void* fnPtr; // JNI 层对应函数的函数指针,注意它是void* 类型.
}
接下来看看MediaScanner JNI 层是如何做的:
static const JNINativeMethod gMethods[] =
......
{
"processFile", // Java 中native 函数的函数名
// processFile 的签名信息,签名信息的知识后面再做解释
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)Z",
(void *)android_media_MediaScanner_processFile // JNI 层对应的函数指针
},
......
{
"native_init",
"()V",
(void *)android_media_MediaScanner_native_init
},
......
};
// 注册JNINativeMethod 数组
// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaScanner(JNIEnv *env)
{
// 调用AndroidRunTime 的registerNativeMethods 函数,第二个参数表明是Java 中的哪个类
return AndroidRuntime::registerNativeMethods(env,
kClassMediaScanner, gMethods, NELEM(gMethods));
}
AndroidRunTime 类提供类一个registerNativeMethods
函数来完成注册工作,代码如下
/*
* Register native methods using JNI.
*/
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
// 调用jniRegisterNativeMethods 函数完成注册
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
其中jniRegisterNativeMethods
是Android平台中为了方便JNI 使用而提供的一个帮助函数:
init jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = (*env) ->FindClass(env, className);
......
// 实际上是调用JNIEnv 的RegisterNative 函数完成注册的
if ((*env) ->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
return -1;
}
return 0;
}
总结如下:
/*
env 指向一个JNIEnv 结构体,它非常重要,后面会讨论它。className 为对应的Java 类名,由于JNINativeMethod 中使用的函数名并非全路径名,所以要指明是哪个类。
*/
clazz = (*env) ->FindClass(env, className);
// 调用JNIEnv 的RegisterNatives 函数,注册关联关系。
(*env) ->RegisterNatives(env, clazz, gMethods, numMethods);
当Java 层通过
System.loadLibrary
加载完JNI 动态库后,紧接着会查找该库中一个叫JNI_Onload
的函数。如果有,就调用它,而动态注册的工作就是在这里完成的。
所以,要使用动态注册的方法,还必须实现JNI_Onload
函数,只有在这个函数中才有机会完成动态注册的工作。静态注册的方法则没有这个要求,但建议大家也实现这个JNI_Onload
函数,因为有一些初始化是可以在这里做的。libmedia_jni.so 的JNI_Onload
函数在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) {
ALOGE("ERROR: GetEnv failed\n");
goto bail;
}
......// 动态注册MediaScanner 的JNI 函数
if (register_android_media_MediaPlayer(env) < 0) {
ALOGE("ERROR: MediaPlayer native registration failed\n");
goto bail;
}
......
/* success -- return valid version number */
result = JNI_VERSION_1_4;// 必须返回这个值,否则会报错。(WHY?????)
bail:
return result;
}
JNI 函数注册的相关内容介绍完了,下面来关注以下JNI 技术中其他几个重要部分。
注意
JNI 层代码中一般要包含jni.h 这个头文件。Android 源码中提供了一个帮助头文件的JNIHelp.h,它内部其实就包含了jni.h,所以我们在自己的代码中直接包含这个JNIhelp.h 即可。
Java 数据类型分析分为基本数据类型和引用数据类型两种,JNI 层也是区别对待二者的。
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 引用类型 | Native 类型 |
---|---|
All obejects | jobject |
java.lang.Class 实例 | jclass |
java.lang.String 实例 | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
java.lang.Throwable 实例 | jthrowable |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
flaot[] | jfloatArray |
double[] | jdoubleArray |
除了Java 中基本数据类型数组、Class、String 和Throwable 外,其余所有Java 对象的数据类型在JNI 中都用jobject 表示。
看看processFile 这个函数:
// Java 层processFile 有三个参数
processFile(String path, String mimeType, MediaScannerClient cliet);
android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
从上面代码中可以发现
如果对象类型都用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 是一个与线程相关的代表JNI 环境的结构体
由上图可知,JNIEnv 实际上就是提供了一些JNI 系统函数。通过这些函数可以做到:
JNIEnv 是一个与线程相关的变量,也就是说,线程A 有一个JNIEnv,线程B 有一个JNIEnv。由于线程相关,所以不能在线程B 中使用线程A 的JNIEnv 结构体。
前面的JNI_OnLoad 函数,它的第一个参数书JavaVM,它是虚拟机在JNI 层的代表,代码如下所示:
// 全进程只有一个JavaVM 对象,所以可以保存,并且在任何地方使用都没有问题
jint JNI_Onload(JavaVM* vm, void* reserved)
其中JavaVM 和JNIEnv 的关系如下:
- 调用JavaVM 的
AttachCurrentThread
函数,就可得到这个线程JNIEnv 结构体。这样就可以在后台线程中回调Java 函数了。- 另外,在后台线程退出前,需要调用JavaVm 的DetachCurrentThread 函数来释放对应的资源。
jfieldID GetFiledID(jclass clazz, const char *name, counst char *sig);
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
其中,jclass 代表Java 类,name 表示成员函数或成员变量的名字,sig 为这个函数和变量的签名信息。如前所示,成员函数和成员变量都是类的信息,这两个函数的第一个参数都是jclass。看一下MS 中的使用:
MyMediaScannerClient(JNIEnv *env, jobject client)......
{
// 先找到android.media.MediaScannerClient 类在JNI 层中对应的jclass 实例。
jclass mediaScannerClientInterface =
env->FindClass(kClassMediaScannerClient);
if (mediaScannerClientInterface == NULL) {
ALOGE("Class %s not found", kClassMediaScannerClient);
} else {
// 取出MediaScannerClient 类中函数scanFile 的jMethodID
mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface,
"scanFile",
"(Ljava/lang/String;JJZZ)V");
// 取出MediaScannerClient 类中函数handleStringTag 的jMethodID
mHandleStringTagMethodID = env->GetMethodID(
mediaScannerClientInterface,
"handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
......
}
}
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
path, lastModified, fileSize, isDirectory);
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
/*
调用JNIEnv 的CallVoidMethod 函数,注意CallVoidMethod的参数:
第一个是代表MediaScannerClient的jobject对象,
第二个参数是函数scanFile 的jmethodId,后面是Java 中scanFile 的参数。
*/
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
mEnv->DeleteLocalRef(pathStr);
return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}
即是通过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 函数,则:
// 获得fieldID 后,可调用GetField 系列函数获取jobject 对应的成员变量的值。
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldId fieldId)
// 或者调用SetField 系列函数来设置jobject 对应的成员变量。
void Set<tyoe>Field(JNIEnv *env, jobject obj, jfieldId filedId, NativeType value)
下面列出一些常见的Get/Set 函数:
[Get/Set]ObjectField()
[Get/Set]BooleanField()
[Get/Set]ByteField()
[Get/Set]CharField()
[Get/Set]ShortField()
[Get/Set]IntField()
[Get/Set]LongField()
[Get/Set]FloatField()
[Get/Set]DoubleField()
Java 中的String 也是引用类型,不过由于它的使用频率较高,所以在JNI 规范中单独创建来一个jstring 类型来表示Java 中的String类型。虽jstring 是一种独立的数据类型,但是它并没有提供成员函数以便操作。而C++ 中的string 类是有自己的成员函数的。这里需要依靠JNIEnv 提供帮助操作jstring:
- 调用JNIEnv 的
NewString(JNIEnv *env, const jchar *unicodeChars, jsize len)
,可以从Native 的字符串得到一个jstring 对象。其实,可以把一个jstring 对象看成是Java 中String 对象在JNI 层的代表,也就是说,jstirng 就是一个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 的内部实现有关。
static jboolean
android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path,
jstring mimeType, jobject client)
{
ALOGV("processFile");
// Lock already hold by processDirectory
MediaScanner *mp = getNativeScanner_l(env, thiz);
......
// 调用JNIEnv 的GetStringUTFChars 得到本地字符串pathStr
const char *pathStr = env->GetStringUTFChars(path, NULL);
......
// 使用完后,必须调用ReleaseStringUTFChars 释放资源
env->ReleaseStringUTFChars(path, pathStr);
......
return result != MEDIA_SCAN_RESULT_ERROR;
}
先来看动态注册中的一段代码
static JNINativeMethod gMethods[] = {
......
{
"processFile",
// processFile 的签名信息
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)Z",
(void *)android_media_MediaScanner_processFile
}
}
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/languageString; | 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 成员和函数的签名信息。
我们知道,Java 中创建的对象最后是由垃圾回收器来回收和释放内存的,可它对JNI 有什么影响呢?
static jobject save_thiz = NULL; // 定义一个全局的jobject
static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
......
// 保存Java 层传入的jobject 对象,代表MediaScanner 对象
save_thiz = thiz;
......
return;
}
// 假设在某个世界,有地方调用callMethodScanner 函数
void callMethodScanner()
{
// 在这个函数中操作save_thiz???
}
上面的做法是肯定会有问题的,因为和save_thiz 对应的Java 层中的MediaScanner 很有可能已经被垃圾回收了,也就是说,save_thiz 保存的这个jobject 可能是一个野指针,如果使用它,后果会很严重。
一般情况下,对一个引用类型执行赋值操作,它的引用计数会增加,而垃圾回收机制只会保证那些没有被引用的对象才会被清理。但是如果在JNI 层使用下面这样的语句,是不会增加引用计数的。
save_thiz = thiz; // 这种赋值不回增加jobject 的引用计数。
引用计数没有增加,thiz就有可能被回收。JNI 规范已很好地解决了这一问题,JNI 计数一共提供了三种类型的引用,它们分别是:
- Local Reference:本地引用。在JNI 层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject 和在 JNI 层函数中创建的jobject。Local Reference 最大的特点就是,一旦JNI 层函数返回,这些jobject 就可能被垃圾回收。
- Global Reference:全局引用,这种对象如不主动释放,它永远不回被垃圾回收。
- Weak Global Reference:弱全局引用,一种特殊的Global Reference,在运行过程中可能会被垃圾回收。所以在使用它之前,需要调用JNIEnv 的IsSameObject 判断它释放被回收了。
平时用得最多的是Local Reference 和 Global Reference,看一下MS 的例子:
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
// 调用NewGlobalRef 创建一个Global Reference,这样mClient 就不用担心被回收了。
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
......
}
// 析构函数
virtual ~MyMediaScannerClient()
{
ALOGV("MyMediaScannerClient destructor");
mEnv->DeleteGlobalRef(mClient); // 调用DeleteGlobalRef 释放这个全局引用
}
每当JNI 层想要保存Java 层中的某个对象时,就可以使用Global Reference,使用完后记住释放就可以了。
下面是Local Reference 的相关实例:
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
path, lastModified, fileSize, isDirectory);
jstring pathStr;
// 调用NewStringUTF 创建一个jstring 对象,它是Local Reference 类型。
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
// 调用Java 的scanFile 函数,把这个jstring 传进去
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
/*
根据Local Reference 的说明,这个函数返回后,pathStr 对象就会被回收,所以下面这个DeleteLocalRef 调用看起来是多余的,其实不然,原因如下:
1) 如果不调用DeleteLocalRef, pathStr 将在函数返回后被回收。
2) 如果调用DeleteLocalRef, pathStr 会立即被回收,这两者还是有区别的。
如果代码如下面这样执行,虚拟机的内存就会很快被耗尽
for (int i = 0; i < 100; i++) {
jstring pathStr = mEnv->NewStringUTF(path));
......// 做一些操作
//mEnv->DeleteLocalRef(pathStr);// 不立即释放Local Reference
}
如果上面代码不调用DeleteLocalRef,则会创建100个jstring,那么内存的耗费就非常可观了。
*/
mEnv->DeleteLocalRef(pathStr);
return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}
JNI 中也有异常处理,不过和C++、java 的一场不太一样。如果调用JNIEnv 的某些函数出错了,则会产生一个异常,但这个异常不会中断本地函数的执行,直到JNI 层返回到Java 层后,虚拟机才会抛出异常。虽然在JNI 层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些清理工作了(例如释放全局引用,或者ReleaseStringChars)。如果这时调用除上面所说的函数之外的其他JNIEnv 函数,则会导致程序死掉。
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
path, lastModified, fileSize, isDirectory);
jstring pathStr;
// NewStringUTF 调用失败后,直接返回,不能再干别的事情了。
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
......
}
JNI 层函数可以在代码中截获和修改这些异常,JNIEnv 提供了三个函数给予帮助:
- ExceptionOccured 函数,判断是否发生异常。
- ExceptionClear 函数,清理当前JNI 层中发生的异常。
- ThowNew 函数,向Java 层抛出异常。
参考文章:
[1] 邓平凡.深入理解Android-卷Ⅰ.北京:机械工业出版社.2011-9