《深入理解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. 基本数据类型的转换
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位
  1. 引用数据类型的转换
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)

从上面代码中可以发现

  • 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 环境的结构体

《深入理解Android 卷Ⅰ》深入理解JNI_第1张图片

由上图可知,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 后,可调用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()

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 类型
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 成员和函数的签名信息。

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

你可能感兴趣的:(深入理解Android,Android,JNI,深入理解Android)