《深入理解Android 卷Ⅰ》深入理解JNI


本人正在学习邓平凡所写的《深入理解Android 卷Ⅰ》,在此做下笔录。



1.1 JNI概述

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

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

也就是说JNI技术就是连接Native 世界和Java 世界的桥梁。如下图所示,展示了Android 平台上JNI 所处的位置:

在这里插入图片描述

1.2 JNI 实例:MediaScanner

下图展示了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 交互。

1.3 Java 层的MediaScanner 分析

先来看MediaScanner(简称MS)的源码,这里将提取出与JNI 有关的部分,其代码如下所示

  • MediaScanner.java
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 函数。

1.3.1 加载JNI 库

这个问题没有标准答案。

原则上是,在调用native 函数前,任何时候、任何地方加载都可以。通行的做法是在类的static 语句中加载,调用System.LoadLibrary 方法就可以了。这一点在上面的代码中也看到。另外,System.loadLibrary 函数的参数是动态库的名字,即media_jni。系统会自动根据不同的平台拓展成真实的动态库文件名,例如在Linux 系统上会拓展成libmedia_jni.so,而在Windows 平台上则会拓展成media_jni.dll。

1.4 JNI 层MediaScanner 的分析

MS的JNI层代码在android_media_MediaScanner.cpp 中,如下所示:

  • 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 函数呢?下面就来回答这个问题。

1.4.1 注册JNI 函数

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 函数的注册方法实际上有两种,下面分别做介绍。

1. 静态方法

整体流程如下:

  • 先编写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 层对应函数的函数指针是否可以完成注册?下面介绍第二种方法:动态注册法。

2. 动态注册

在JNI 技术中,用来记录Java native 函数和JNI 函数这种一一对应关系的,是一个叫JNINativeMethod 的结构,其定义如下:

typedef struct {
  // Java 中Native 函数的名字,不用携带包的路径,例如"native_init".
  count char* name;
  // Java 函数的签名信息,用字符串表示,是参数类型和返回值类型的组合.
  const char* signature;
  void* fnPtr; // JNI 层对应函数的函数指针,注意它是void* 类型.
}

接下来看看MediaScanner JNI 层是如何做的:

  • andorid_meida_MediaScanner.cpp
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 函数来完成注册工作,代码如下

  • AndroidRunTime.cpp
/*
 * 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 使用而提供的一个帮助函数:

  • JNIHelp.c
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中:

  • 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 即可。

1.4.2 数据类型转换

Java 数据类型分析分为基本数据类型和引用数据类型两种,JNI 层也是区别对待二者的。

  1. 基本数据类型的转换
JavaNative 类型符号属性字长
booleanjboolean无符号8位
bytejbyte无符号8位
charjchar无符号16位
shortjshort有符号16位
intjint有符号32位
longjlong有符号64位
floatjfloat有符号32位
doublejdouble有符号64位
  1. 引用数据类型的转换
Java 引用类型Native 类型
All obejectsjobject
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)

从上面代码中可以发现

  • Java 的String 类型在JNI 层对应位jstring 类型。
  • Java 的MediaScannerClient 类型在JNI 层对应为object。

如果对象类型都用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)

1.4.3 JNIEnv 介绍

JNIEnv 是一个与线程相关的代表JNI 环境的结构体

在这里插入图片描述

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

  • 调用Java 的函数。
  • 操作jobject 对象等很多事情。

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 函数来释放对应的资源。

1.4.4 通过JNIEnv 操作jobject

  1. jfieldID 和jmethodID 介绍
    成员变量和成员函数都是由类定义的,它们是类的属性,所以在JNI 规则中,用jfieldID 和jmethodID 来表示Java 类成员变量和成员函数,可通过JNIENv 的下面两个函数得到:
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 中的使用:

  • android_media_MediaScanner.cpp::MyMediaScannerClient 构造函数
    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");

            ......
        }
    }
  1. 使用jfieldID 和jmethodID
    下面再看一个例子:
  • android_media_MediaScanner.cpp::MyMediaScannerClient 的scanFile
    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 函数的返回值类型,例如CallIntMethodCallVoidMethod 等。
上面是针对非static 函数的,如果想调用Java 中的static 函数,则:

// 获得fieldID 后,可调用Get<type>Field 系列函数获取jobject 对应的成员变量的值。
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldId fieldId)
// 或者调用Set<type>Field 系列函数来设置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()

1.4.5 jstring 介绍

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 的内部实现有关。
  • android_media_MediaScanner.cpp
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;
}

1.4.6 JNI 类型签名介绍

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

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 类型
ZbooleanFfloat
BbyteDdouble
CcharL/java/languageString;String
Sshort[Iint[]
Iint[L/java/lang/object;Object[]
Jlong

请注意,如果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 成员和函数的签名信息。

1.4.7 垃圾回收

我们知道,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 的例子:

  • andorid_media_MediaScanner.cpp::MyMediaScannerClient 构造函数
    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 的相关实例:

  • andorid_media_MediaScanner.cpp::MyMediaScannerClient 的 scanFile
    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");
    }

1.4.8 JNI 中的异常处理

JNI 中也有异常处理,不过和C++、java 的一场不太一样。如果调用JNIEnv 的某些函数出错了,则会产生一个异常,但这个异常不会中断本地函数的执行,直到JNI 层返回到Java 层后,虚拟机才会抛出异常。虽然在JNI 层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些清理工作了(例如释放全局引用,或者ReleaseStringChars)。如果这时调用除上面所说的函数之外的其他JNIEnv 函数,则会导致程序死掉。

  • andorid_media_MediaScanner.cpp::MyMediaScannerClient 的 scanFile
    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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值