【Android 系统开发】_“核心技术”篇 -- JNI

xuweinet 2019-06-28

开篇

核心源码

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

JNI概述

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

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

在平台无关的Java中,为什么要创建一个与Native相关的JNI技术呢?这岂不是破坏了Java的平台无关特性吗?JNI技术的推出主要有以下几个方面的考虑:
      ✨ 承载Java世界的虚拟机是用Native语言写的,而虚拟机又运行在具体的平台上,所以虚拟机本身无法做到平台无关。然而,有了JNI技术后,就可以对Java层屏蔽不同操作系统平台之间的差异了。这样,就能实现Java本身的平台无关特性。
      ✨ 在Java诞生之前,很多程序都是用Native语言写的,随后Java后来受到追捧,并且迅速发展,但是作为一门高级语言,无法将软件世界彻底的改变。那么既然Native模块实现了许多功能,那么在Java中直接通过JNI技术去使用它们不久可以了?

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

原理图如下:

【Android 系统开发】_“核心技术”篇 -- JNI

一律的讲原理很枯燥,我们直接以实际的代码作为范例来学习JNI的原理和实际使用!

MediaScanner

如果你是做Android系统开发和维护工作的,那么你肯定听过MediaScanner,那我们就拿它来举例,看看它和JNI之间是如何关联的。

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

MediaScanner和它的JNI:

【Android 系统开发】_“核心技术”篇 -- JNI

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

      ✨ 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交互。

源码分析 - Java层

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 void processFile(String path, String mimeType, MediaScannerClient client);
    ... ...
    
    private static native final void native_init();  // 申明一个native函数。native为Java的关键字,表示它由JNI层实现。

    ... ...
}

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

加载JNI库

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

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

native函数

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

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

源码分析 - JNI层

实现函数

接下来我们看下Java层中定义的两个native函数在JNI层的实现。

native_init的JNI层实现

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

static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }

    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

processFile的JNI层实现

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

这边我们来解答一个问题,我们确实是知道MediaScanner的native函数是JNI层去实现的,但是系统是如何知道Java层的native_init函数对应的就是JNI层的android_media_MediaScanner_native_init函数呢?

注册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函数,它需要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了?

这就是下面我们要介绍的第二种方法:动态注册法!

动态方法注册

我们知道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;)V",  // 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的函数。如果有,就调用它,而动态注册的工作就是在这里完成的。

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

数据类型转换

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

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

基本数据类型的转换

Java基本类型Native类型符号属性字长
booleanjboolean无符号8位
bytejbyte无符号8位
charjchar无符号16位
shortjshort有符号16位
intjint有符号32位
longjlong有符号64位
floatjfloat有符号32位
doublejdoublt有符号64位

引用数据类型的转换

Java引用类型Native类型Java引用类型Native类型
All objectsjobjectchar[]jcharArray
java.lang.Class 实例jclassshort[]jshortArray
java.lang.String 实例jstringint[]jintArray
Object[]jobjectArraylong[]jlongArray
boolean[]jbooleanArrayfloatjfloatArray
byte[]jbyteArraydouble[]jdoubleArray
java.lang.Throwable 实例jthrowable

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

private native void processFile                   (                           String  path,  String  mimeType,  MediaScannerClient client);
static void 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)

接下来我们开始重点讨论JNIEnv!!!

JNIEnv

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

JNIEnv的作用
      ✨ 调用 Java 函数 : JNIEnv 代表 Java 执行环境, 能够使用 JNIEnv 调用 Java 中的代码
      ✨ 操作 Java 对象 : Java 对象传入 JNI 层就是 Jobject 对象, 须要使用 JNIEnv 来操作这个 Java 对象

我们来看一个有趣的现象

前面,我们已经知道 JNIEnv 是一个与线程相关的变量,如果此时线程 A 有一个 JNIEnv 变量, 线程 B 也有一个JNIEnv变量,由于线程相关,所以 A 线程不能使用 B 线程的 JNIEnv 结构体变量。
此时,一个java对象通过JNI调用动态库中的一个send()函数向服务器发送消息,不等服务器消息到来就立即返回,同时把JNI接口的指针JNIEnv *env(虚拟机环境指针),和jobject obj保存在动态库中的变量里。一段时间后,动态库中的消息接收线程接收到服务器发来的消息,并试图通过保存过的env和obj来调用先前的java对象的方法(相当于JAVA回调方法)来处理此消息,此时程序突然退出(崩溃)

为什么?

原因:前台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函数来释放对应的资源。

通过JNIEnv操作jobject

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

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

取出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并保存起来以供后续使用。

使用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输出的CallStatic<Type>Method系列函数。

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

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

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

垃圾回收及异常处理

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

参考Blog

                  Java Native Interface (JNI)

相关推荐

ElvenShi / 0评论 2012-11-12