【深入理解Android】以MediaScanner为例学习JNI

本文转载自邓凡平的《深入理解Android卷I》第三章深入理解JNI

目录

1. JNI概述

2. 学习JNI的实例:MediaScanner

3. Java 层的 MediaScanner

3.1  加载JNI库

3.2 Java的native函数和总结

4. JNI 层的 MediaScanner

4.1 注册JNI函数

4.1.1 静态方法,使用javah,流程如下:

4.2.2 动态注册,使用JNINativeMethod结构,定义如下:

4.2 数据类型转换

4.2.1 基本类型的转换

4.2.2 引用数据类型的转换

4.3 JNIEnv介绍

4.4 通过JNIEnv操作jobject

4.4.1 jfieldID和jmethodID的介绍

4.4.2 jfieldID和jmethodID的使用

4.5 jstring介绍

4.6 JNI类型签名的介绍

4.7 垃圾回收

4.8 JNI中的异常处理


1. JNI概述

JNI是Java Native Interface (java本地调用) 的缩写,这种技术可以做到以下两点:

  1. java程序中函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数;
  2. Native程序中的函数可以调用Java层的函数,也就是说,C/C++程序中可以调用java的函数。

2. 学习JNI的实例:MediaScanner

  1. java世界对应的是MediaScanner,然而MediaScanner类中的一些函数需要Native层来实现;
  2. JNI层对应的是libmedia_jni.so。media_jni是JNI库的名字,其中,下划线前的“media”是Native层库的名字,这里就是libmedia库。下划线后的“jni”表示他是一个JNI库。注意,JNI库名字可以随便取,但是Android平台上基本都采用“lib模块名_jni.so” 的命名方式;   
  3. Native层对应的是libmedia.so,这个库完成了实际的功能;   
  4. MediaScanner将通过JNI库libmedia_jni.so和Native层的libmedia.so交互;
  5. 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函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。

弊端:

  1. 需要编译所有声明了native函数的Java类,每个所生成的class文件都得用javah生成一个头文件。  
  2. javah生成的JNI层函数名特别长,书写起来很不方便。    
  3. 初次调用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层代码必须关注的事情,在编写代码时一定要小心对待。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值