转载

Android 8.1 源码_核心篇(一) -- 深入研究 JNI

开篇

核心源码

关键类 路径
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

JNI概述

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

:sparkles: Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写函数;

:sparkles: Native程序中的函数可以调用Java层的函数,也就是说在C/C++程序中可以调用Java的函数;

在平台无关的Java中,为什么要创建一个与Native相关的JNI技术呢?这岂不是破坏了Java的平台无关特性吗?JNI技术的推出主要有以下几个方面的考虑:

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

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

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

原理图如下:

Android 8.1 源码_核心篇(一) -- 深入研究 JNI

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

MediaScanner

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

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

MediaScanner和它的JNI:

Android 8.1 源码_核心篇(一) -- 深入研究 JNI

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

:sparkles: Java世界对应的是MediaScanner,而这个MediaScanner类有一些函数需要由Native层来实现(定义了一些Native函数,具体实现代码在Native层)

:sparkles: JNI层对应的是libmedia_jni.so。

· media_jni是JNI库的名字,其中下划线前的“media”是Native层库的名字,这里就是libmedia库。下划线后的“jni”表示它是一个JNI库。

· Android平台基本上都采用“lib模块名_jni.so”来命名JNI库。

:sparkles: Native层对应的是libmedia.so,这个库完成了实际的功能。

:sparkles: 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参与,整体流程如下:

:sparkles: 先编写Java代码,然后编译生成.class文件。

:sparkles: 使用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函数的名字必须遵循特定的格式。

这种方法有三个弊端,如下:

:sparkles: 需要编译所有声明了native函数的Java类,每个所生成的class文件都得用javah生成一个头文件;

:sparkles: javah生成的JNI层函数名特别长,书写起来很不方便;

:sparkles: 初次调用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类型 符号属性 字长
boolean jboolean 无符号 8位
byte jbyte 无符号 8位
char jchar 无符号 16位
short jshort 有符号 16位
int jint 有符号 32位
long jlong 有符号 64位
float jfloat 有符号 32位
double jdoublt 有符号 64位

引用数据类型的转换

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函数:

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)

我们发现:

:sparkles: Java的String类型在JNI层对应为jstring类型;

:sparkles: 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在本线程的执行环境。

JNUEnv的作用

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

:sparkles: 操作 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:

:sparkles: 调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体,这样就可以在后台线程中回调Java函数了。

:sparkles: 另外,在后台线程退出前,需要调用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的函数:

:sparkles: 调用JNIEnv的NewString(const jchar* unicodeChars, jsize len),可以从Native的字符串得到一个jstring对象。

:sparkles: 调用JNIEnv的NewStringUTF(const char* bytes)将根据Native的一个UTF-8字符串得到一个jstring对象。

:sparkles: 上面两个函数将本地字符串转换成了Java的String对象,JNIEnv还提供了GetStringChars函数和GetStringUTFChars函数,它们可以将Java String对象转换成本地字符串。其中GetStringChars得到一个Unicode字符串,而GetStringUTFChars得到一个UTF-8字符串。

:sparkles: 另外,如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用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)

原文  https://segmentfault.com/a/1190000016101369
正文到此结束
Loading...