必须了解的 "JNI"


1. 开篇

1.1 核心源码( Android 9.0 )

关键类 路径
MediaScanner.java frameworks/base/media/java/android/media/MediaScanner.java
android_media_MediaScanner.cpp frameworks/base/media/jni/android_media_MediaScanner.cpp
android_media_MediaPlayer.cpp frameworks/base/media/jni/android_media_MediaPlayer.cpp
AndroidRuntime.cpp frameworks/base/core/jni/AndroidRuntime.cpp

1.2 JNI

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

      ✎ Java 程序中的函数可以调用 Native 语言写的函数,Native 一般指的是 C/C++ 编写函数(在源码中我们经常能看到从 Java 层跳转到 Native层的函数)
      ✎ Native 程序中的函数可以调用 Java 层的函数,也就是说在 C/C++ 程序中可以调用 Java 的函数;

我们一直说 Java 的优势在于它的平台无关性,那么为什么还要创建一个与 Native 相关的 JNI 技术呢?

JNI 技术的推出主要有以下几个方面的考虑:

      ✎ 承载 Java 世界的虚拟机是用 Native 语言写的,而虚拟机又运行在具体的平台上,所以虚拟机本身无法做到平台无关。然而,有了 JNI 技术后,就可以对 Java 层屏蔽不同操作系统平台之间的差异了。这样,就能实现 Java 本身的平台无关特性。

      ✎ 在 Java 诞生之前,很多程序都是用 Native 语言编写,后来 Java 受到追捧并且迅速发展。但是作为一门高级语言,无法将软件世界彻底的改变。那么既然 Native 模块实现了许多功能,那么在 Java 中直接通过 JNI 技术去使用它们就可以了。

所以,我们可以把 JNI 看作一座将 Native 世界和 Java 世界互联起来的一座桥梁特殊说明:JNI 层的代码也是用 Native 写的!)。

我们看下示意图:

ezVMnJ.png

1.3 MediaScanner

如果你是 Android 系统开发工程师,那么你肯定知道 MediaScanner,本篇文章就拿它来举例,看看它和 JNI 之间是如何关联的。

MediaScanner 是 Android 平台中多媒体系统的重要组成部分,它的功能是扫描媒体文件,得到诸如歌曲时长、歌曲作者等媒体信息,并将他们存入到媒体数据库中,供其他应用程序使用。

MediaScanner 和它的 JNI:

ezVGh6.png

我们简单说明下这个流程图:

      ✎ Java 世界对应的是 MediaScanner,而这个 MediaScanner 类有一些函数需要由 Native 层来实现(定义了一些 Native 函数,具体实现代码在 Native层)。
      ✎ JNI 层对应的是 libmedia_jni.so
             ● “media_jni” 是 JNI 库的名字,其中下划线前的 “media” 是 Native 层库的名字,这里就是 “libmedia” 库。下划线后的 “jni” 表示它是一个 JNI 库。
             ● Android 平台基本上都采用 “lib模块名_jni.so” 来命名 JNI 库。
      ✎ Native 层对应的是 libmedia.so,这个库完成了实际的功能。
      ✎ MediaScanner 将通过 JNI 库 libmedia_jni.so 和 Native 层的 libmedia.so 交互。


2. 源码分析 - Java层

2.1 MediaScanner.java

我们先来看看 MediaScanner 在 Java 层中关于 JNI 的代码:

package android.media;

public class MediaScanner implements AutoCloseable {
    static {                        // static语句
        // media_jni 为 JNI 库的名字,实际加载动态库的时候会将其拓展成 libmedia_jni.so
        System.loadLibrary("media_jni");    
        native_init();              // 调用 native_init 函数
    }
    ... ...

    private native boolean processFile(String path, String mimeType, MediaScannerClient client);

    // 申明一个 native 函数。native 为 Java 的关键字,表示它由 JNI 层实现。
    private static native final void native_init();
    ... ...
}

OK,以上代码列出了两个重要的要点:(1)加载 JNI 库;(2)调用 Java 的 native 函数。

2.2 加载 JNI 库

我们前面说到过,如果 Java 要调用 native 函数,就必须通过一个位于 JNI 层的动态库来实现。那么这个动态库在什么时候、什么地方加载?

原则上,在调用 native 函数之前,我们可以在任何时候、任何地方去加载动态库。但一般通行的做法就是在类的 static 语句中加载,调用 System.loadLibrary 方法就可以了。

2.3 native 函数

我们发现 native_init 和 processFile 函数前面都有 Java 的关键字 native,这个就表示函数将由 JNI 层来实现。

所以在 Java 层面去使用 JNI 只要做两项工作:(1)加载对应的 JNI 库;(2)申明由关键字 native 修饰的函数。

3. 源码分析 - JNI层

3.1 实现函数

接下来我们从源码角度分析 Java 层中定义的两个 native 函数在 JNI 层的实现。

native_init 的 JNI 层实现

// frameworks/base/media/jni/android_media_MediaScanner.cpp

static const char* const kClassMediaScanner = "android/media/MediaScanner";

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;
    }
}

processFile 的 JNI 层实现

// frameworks/base/media/jni/android_media_MediaScanner.cpp

static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    // Lock already hold by processDirectory
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    ... ...

    const char *pathStr = env->GetStringUTFChars(path, NULL);
    ... ...

    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
    return result != MEDIA_SCAN_RESULT_ERROR;
}

我们确实是知道 MediaScanner 的 native 函数是 JNI 层去实现的,但是系统是如何知道 Java 层的 native_init 函数对应的就是 JNI 层的 android_media_MediaScanner_native_init 函数呢?这就涉及到 JNI 函数的注册问题,我们接着往下看!

3.2 注册 JNI 函数

不知道你有没有注意到 native_init 函数位于 android.media 这个包中,它的全路径名应该是 android.media.MediaScanner.native_init,而 JNI 层函数的名字是 android_media_MediaScanner_native_init

是不是很神奇?名字对应着,唯一的区别就是 "." 这个符号变成了 "_"。因为在 Native 语言中,符号 “.” 有着特殊的意义,所以 JNI 层需要把 Java 函数名称(包括包名)中的 “.” 换成 “_”。也就是通过这种方式,native_init 找到了自己 JNI 层的本家兄弟 android.media.MediaScanner.native_init。

现在我们知道了 Java 层 native 函数对应 JNI 层的函数的原理,但有个问题,我们确实知道是调用了哪个函数,但是两个函数是怎么关联起来起来的?这就涉及到 JNI 函数注册的问题。

JNI 函数有两种注册方式:(1)静态注册;(2)动态注册。

3.2.1 静态注册

这种方法很简单,很暴力!直接根据函数名来找对应的 JNI 函数,它需要 Java 的工具程序 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

/* DO NOT EDIT THIS FILE - it is machine generated*/
  #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 JNICALL Java_android_media_MediaScanner_processFile(JNIEnv *, jobject, jstring, jstring, jobject);
... ...

// native_init 对应的 JNI 函数
JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_linit(JNIEnv *, jclass);

#ifdef _cplusplus
}
#endif
#endif

从上面代码中可以发现,native_init 和 processFile 的 JNI 层函数被声明成:

// Java 层函数名中如果有一个"_", 转换成 JNI 后就变成了 "l"
JNIEXPORT void JNICALL Java_android_media_MediaScanner_processFile
JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_linit

Ok,那么静态方法中 native 函数是如何找到对应的 JNI 函数的呢?

比如,当 Java 层调用 native_init 函数时,它会从对应的 JNI 库中寻找 Java_android_media_MediaScanner_native_init 函数,如果没有,就会报错。如果找到,则会为这个 native_init 和 Java_android_media_MediaScanner_native_init 建立一个关联关系,其实就是保存 JNI 层函数的函数指针。以后再调用 native_init 函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。

从这里可以看出,静态方法就是根据函数名来建立 Java 函数与 JNI 函数之间的关联关系的,而且它要求 JNI 函数的名字必须遵循特定的格式。

这种方法有三个弊端:

      ✎ 需要编译所有声明了 native 函数的 Java 类,每个所生成的 class 文件都得用 javah 生成一个头文件;
      ✎ javah 生成的 JNI 层函数名特别长,书写起来很不方便;
      ✎ 初次调用 native 函数时需要根据函数名称搜索对应的 JNI 函数来建立关联关系,这样会影响运行效率。

所以我们是否有办法克服以上三点弊端?

我们知道静态方法是通过建立函数关联关系,以后再根据这个函数指针去调用对应的 JNI 函数,那么如果我们直接让 native 函数直接知道 JNI 层对应函数的函数指针,不就 ok 了?

3.2.2 动态注册

我们知道 Java native 函数和 JNI 函数是一一对应的,这个就像我们 key 对应 value 一样,那么如果有一个结构来保存这种关联关系,通过这个结构直接可以找到彼此的关联,是不是效率就高多了?

答案是肯定的,动态注册就是这么干的!在 JNI 技术中,用来记录这种一一对应关系的,是一个叫 JNINativeMethod 的结构,其定义如下:

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

如何使用这个结构体,我们看下 MediaScanner JNI 层是如何做的:

// 定义一个 JNINativeMethod 数组,其成员就是 MediaScanner 中所有 native 函数的一一对应关系。
static const JNINativeMethod gMethods[] = {
    ... ...

    {
        "processFile",                                        // Java 中 native 函数的函数名
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)Z",  // processFile 的签名信息
            (void *)android_media_MediaScanner_processFile    // JNI 层对应的函数指针
    },
    ... ...

    {
        "native_init",
        "()V",
            (void *)android_media_MediaScanner_native_init
    },
    ... ...

};

是不是一目了然?定义好了,不能直接用啊,当然需要注册一下。

// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
// 注册 JNINativeMethod 数组
int register_android_media_MediaScanner(JNIEnv *env)
{
    // 调用 AndroidRuntime 的 registerNativeMethods 函数,第二个参数表明是 Java 中的哪个类
    // 我们在讲解 Zygote 原理时,聊过创建 Java 虚拟机,注册 JNI 函数的内容
    return AndroidRuntime::registerNativeMethods(env, kClassMediaScanner, gMethods, NELEM(gMethods));
}

AndroidRunTime 类提供了一个 registerNativeMethods 函数来完成注册的工作,下面来看下 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 使用而提供的一个帮助函数

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;
    }
}

其实动态注册的工作,只用两个函数就能完成,如下:

(1) jclass clazz = (*env)->FindClass(env, className);

         env 指向一个 JNIEnv 结构体,它非常重要,后面我们会讨论。classname 为对应的 Java 类名,由于 JNINativeMethod 中使用的函数名并非全路径名,所以要指明是哪个类。

(2) (*env)->RegisterNatives(env, clazz, gMethods, numMethods);

         调用 JNIEnv 的 RegisterNatives 函数,注册关联关系。

现在,你现在知道如果动态注册了,但是有个问题,这些动态注册的函数又是在什么时候和什么地方被调用的?

当 Java 层通过 System.loadLibrary 加载完 JNI 动态库后,紧接着就会去查找该库中一个叫 JNI_OnLoad 的函数。如果有,就调用它,而动态注册的工作就是在这里完成的。

3.2.3 JNI_OnLoad

动态库是 libmedia_jni.so,那么 JNI_OnLoad 函数在哪里实现的?如果你看的比较仔细的话,我相信之前代码中有段注释你应该注意到了。

// 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)
{
    return AndroidRuntime::registerNativeMethods(env, kClassMediaScanner, gMethods, NELEM(gMethods));
}

由于多媒体系统很多地方都使用了 JNI,所以 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) {
        goto bail;
    }
    ... ...

    if (register_android_media_MediaScanner(env) < 0) {
        ALOGE("ERROR: MediaScanner native registration failed\n");
        goto bail;
    }
    ... ...

    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

bail:
    return result;
}

3.3 数据类型转换

在 Java 中调用 native 函数传递的参数是 Java 数据类型,那么这些参数类型传递到 JNI 层会变成什么类型?

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

3.3.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 jdoublt 有符号 64位

3.3.2 引用数据类型的转换

Java 引用类型 Native 类型 Java 引用类型 Native 类型
All objects jobject char[] jcharArray
java.lang.Class 实例 jclass short[] jshortArray
java.lang.String 实例 jstring int[] jintArray
Object[] jobjectArray long[] jlongArray
boolean[] jbooleanArray float jfloatArray
byte[] jbyteArray double[] jdoubleArray
java.lang.Throwable 实例 jthrowable


我们举例说明,看下 processFile 函数:

// String --> jstring
// MediaScannerClient --> jobject
processFile(String path, String mimeType, MediaScannerClient client);
android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)

我们发现:

      ✎ Java 的 String 类型在 JNI 层对应为 jstring 类型;
      ✎ Java 的 MediaScannerClient 类型在 JNI 层对应为 jobject。

但是有一个问题,Java 中的 processFile 中只有三个参数,为什么到了 JNI 层对应的函数却有五个参数?

static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)

3.4 JNIEnv

(1)JNIEnv 的概念

      是一个与线程相关的代表 JNI 环境的结构体,该结构体代表了 Java 在本线程的执行环境。

(2)JNUEnv 的作用

      ✎ 调用 Java 函数 : JNIEnv 代表 Java 执行环境, 能够使用 JNIEnv 调用 Java 中的代码。

      ✎ 操作 Java 对象 : Java 对象传入 JNI 层就是 Jobject 对象, 须要使用 JNIEnv 来操作这个 Java 对象。

(3)我们来看一个有趣的现象

前面,我们已经知道 JNIEnv 是一个与线程相关的变量,如果此时线程 A 有一个 JNIEnv 变量, 线程 B 也有一个 JNIEnv 变量,由于线程相关,所以 A 线程不能使用 B 线程的 JNIEnv 结构体变量。

此时,一个 java 对象通过 JNI 调用动态库中的一个 send() 函数向服务器发送消息,不等服务器消息到来就立即返回,同时把 JNI 接口的指针 JNIEnv *env 和 jobject obj 保存在动态库中的变量里。一段时间后,动态库中的消息接收线程接收到服务器发来的消息,并试图通过保存过的 env 和 obj 来调用先前的 java 对象的方法来处理此消息,此时程序突然退出(崩溃)

(4)为什么?

出错原因前台 JAVA 线程发送消息,后台线程处理消息,归属于两个不同的线程,不能使用相同的 JNIEnv 变量。

怎么解决?

还记得我们前面介绍的 JNI_OnLoad 函数吗?它的第一个参数是 JavaVM,它是虚拟机在 JNI 层的代表!!!

// 全进程只有一个 JavaVM 对象,所以可以在任何地方使用
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)

那么也就是说,不论进程有多少线程(不论有多少 JNIEnv),JavaVM 却是独此一份!所以,我们可以利用一个机制:利用全局的 JavaVM 指针得到当前线程的 JNIEnv 指针。

JavaVM 和 JNIEnv

      ✎ 调用 JavaVM 的 AttachCurrentThread 函数,就可得到这个线程的 JNIEnv 结构体,这样就可以在后台线程中回调 Java 函数了。
      ✎ 另外,在后台线程退出前,需要调用 JavaVM 的 DetachCurrentThread 函数来释放对应的资源。

3.5 通过 JNIEnv 操作 jobject

前面介绍数据类型的时候,我们知道 Java 的引用类型除了少数几个外(Class、String 和 Throwable),最终在 JNI 层都会用 jobject 来表示对象的数据类型,那么该如何操作这个 jobject 呢?

我们先回顾下 Java 对象是由什么组成的?当然是它的成员变量和成员函数了!那么同理,操作 jobject 的本质就应当是操作这些对象的成员变量和成员函数!那么 jobject 的成员变量和成员函数又是什么?

3.5.1 取出 jfieldID 和 jmethodID

在 Java 中,我们知道成员变量和成员函数都是由类定义的,他们是类的属性,那么在 JNI 规则中,也是这么来定义的,用 jfieldID 定义 Java 类的成员变量,用 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 为这个函数和变量的签名信息(后面会说到)。

我们来看看在 MediaScanner 中如何使用它们,查看 android_media_MediaScanner.cpp::MyMediaScannerClient 构造函数

class MyMediaScannerClient : public MediaScannerClient
{
public:
    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");

            ... ...
        }
    }
    ... ...
    jobject mClient;
    jmethodID mScanFileMethodID;
    jmethodID mHandleStringTagMethodID;
    ... ...
}

从上面的代码中,将 scanFile 和 handleStringTag 函数的 jMethodID 保存在 MyMediaScannerClient 的成员变量中。为什么这里要把它们保存起来呢?这个问题涉及到一个关于程序运行效率的知识点:

如果每次操作 jobject 前都要去查询 jmethodID 或 jfieldID,那么将会影响程序运行的效率,所以我们在初始化的时候可以取出这些 ID 并保存起来以供后续使用。

3.5.2 使用 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)
    {
        jstring pathStr;
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
            mEnv->ExceptionClear();
            return NO_MEMORY;
        }

        /*
         * 调用 JNIEnv 的 CallVoidMethod 函数
         * 注意 CallVoidMethod 的参数:
         *(1)第一个参数是代表 MediaScannerClient 的 jobject 对象
         *(2)第二个参数是函数 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 等。如果想调用 Java 中的 static 函数,则用 JNIEnv 输出的 CallStaticMethod 系列函数。

所以,我们可以看出,虽然 jobject 是透明的,但有了 JNIEnv 的帮助,还是能轻松操作 jobject 背后的实际对象的。

3.6 jstring

这一节我们单独聊聊 String。Java 中的 String 也是引用类型,不过由于它的使用频率很高,所以在 JNI 规范中单独创建了一个 jstring 类型来表示 Java 中的 String 类型。

虽然 jstring 是一种独立的数据类型,但是它并没有提供成员函数以便操作。而 C++ 中的 string 类是有自己的成员函数的。那么该如何操作 jstring 呢?还是得依靠 JNIEnv 提供帮助。

先看几个有关 jstring 的函数:

      ✎ 调用 JNIEnv 的 NewString(const jchar* unicodeChars, jsize len) 可以从 Native 的字符串得到一个 jstring 对象。
      ✎ 调用 JNIEnv 的 NewStringUTF(const char* bytes) 将根据 Native 的一个 UTF-8 字符串得到一个 jstring 对象。
      ✎ 上面两个函数将本地字符串转换成了 Java 的 String 对象,JNIEnv 还提供了 GetStringChars 函数和 GetStringUTFChars 函数,它们可以将 Java String 对象转换成本地字符串。其中 GetStringChars 得到一个 Unicode 字符串,而 GetStringUTFChars 得到一个 UTF-8 字符串。
      ✎ 另外,如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用 ReleaseStringChars 函数或 ReleaseStringUTFChars 函数来对应地释放资源,否认会导致 JVM内存泄漏

我们看段代码加深印象:

static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    // Lock already hold by processDirectory
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    ... ...

    const char *mimeTypeStr =
        (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
    if (mimeType && mimeTypeStr == NULL) {  // Out of memory
        // ReleaseStringUTFChars can be called with an exception pending.
        env->ReleaseStringUTFChars(path, pathStr);
        return;
    }
    ... ...
}

3.7 JNI 类型签名

我们看下动态注册中的一段代码:

static const JNINativeMethod gMethods[] = {
    ... ...

    {
        "processFile",
        // processFile的签名信息,这么长的字符串,是什么意思?
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (void *)android_media_MediaScanner_processFile
    },
    ... ...
};

上面这段代码我们之前早就见过了,不过"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V"是什么意思呢?

我们前面提到过,这个是 Java 中对应函数的签名信息,由参数类型和返回值类型共同组成,有人可能有疑问,这东西是干嘛的?

我们都知道,Java 支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名是没法找到具体函数的。为了解决这个问题,JNI 技术中就将参数类型和返回值类型的组合作为一个函数的签名信息,有了签名信息和函数名,就能很顺利地找到 Java 中的函数了。

JNI 规范定义的函数签名信息看起来很别扭,不过习惯就好了。它的格式是:

(参数 1 类型标识参数 2 类型标识 … 参数 n 类型标识) 返回值类型标识

我们仍然拿 processFile 的例子来看下:

    {
        "processFile",
        // Java 中的函数定义为 private native void processFile(String path, String mimeType, MediaScannerClient client);
        // 对应的 JNI 函数签名如下:
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        // void 类型对应的标示是 V
        // 当参数的类型是引用类型时,其格式是 "L包名",其中包名中的 "."换成 "/",Ljava/lang/String 表示是一个 Java String 类型
        (void *)android_media_MediaScanner_processFile
    },

【注意】:引用类型(除基本类型的数组外)的标识最后都有一个“;”。

函数签名不仅看起来麻烦,写起来更麻烦,稍微写错一个标点都会导致注册失败,所以在具体编码时,可以定义字符串宏(这边就不多做解释了,可以自行查询了解即可)。

虽然函数签名信息很容易写错,但是 Java 提供了一个叫 javap 的工具能够帮助我们生成函数或变量的签名信息,它的用法如下:

javap -s -p xxx

其中 xxx 为编译后的 class 文件,s 表示输出内部数据类型的签名信息,p 表示打印所有函数和成员的签名信息,默认只会打印 public 成员和函数的签名信息。

3.8 垃圾回收及异常处理

这部分我打算单独放在一篇博文中探讨,结果具体错误进行分析。