[深刻理解Android卷一 全文-第二章]深刻理解JNI

因为《深刻理解Android 卷一》和《深刻理解Android卷二》再也不出版,而知识的传播不该该由于纸质媒介的问题而中断,因此我将在OSC博客中全文转发这两本书的所有内容。 java

第2章  深刻理解JNI

本章主要内容 android

·  经过一个实例,介绍JNI技术和在使用中应注意的问题。 程序员

本章涉及的源代码文件名及位置 数据库

下面是本章分析的源码文件名及其位置。 数组

·  MediaScanner.java 网络

framework/base/media/java/src/android/media/MediaScanner.java 函数

·  android_media_MediaScanner.cpp 工具

framework/base/media/jni/MediaScanner.cpp 学习

·  android_media_MediaPlayer.cpp 编码

framework/base/media/jni/android_media_MediaPlayer.cpp

·  AndroidRunTime.cpp

framework/base/core/jni/AndroidRunTime.cpp

·  JNIHelp.c

dalvik/libnativehelper/JNIHelp.c

2.1  概述

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一直在使用JNI技术,只是咱们平时较少用到罢了。

·  早在Java语言诞生前,不少程序都是用Native语言写的,它们遍及在软件世界的各个角落。Java出世后,它受到了追捧,并迅速获得发展,但仍没法对软件世界完全改朝换代,因而才有了折中的办法。既然已经有Native模块实现了相关功能,那么在Java中经过JNI技术直接使用它们就好了,省得落下重复制造轮子的坏名声。另外,在一些要求效率和速度的场合仍是须要Native语言参与的。

在Android平台上,JNI就是一座将Native世界和Java世界间的天堑变成通途的桥,来看图2-1,它展现了Android平台上JNI所处的位置:

图2-1  Android平台中JNI示意图

由上图可知,JNI将Java世界和Native世界紧密地联系在一块儿了。在Android平台上尽情使用Java开发的程序员们不要忘了,若是没有JNI的支持,咱们将步履维艰!

注意,虽然JNI层的代码是用Native语言写的,但本书仍是把和JNI相关的模块单独归类到JNI层。

俗话说,百闻不如一见,就来见识一下JNI技术吧。

 

2.2  经过实例学习JNI

初次接触JNI,感受最神奇的就是,Java居然可以调用Native的函数,可它是怎么作到的呢?网上有不少介绍JNI的资料。因为Android大量使用了JNI技术,本节就将经过源码中的一处实例,来学习相关的知识,并了解它是如何调用Native的函数的。

这个例子,是和MediaScanner相关的。在本书的最后一章,会详细分析它的工做原理,这里先看和JNI相关的部分,如图2-2所示:

图2-2  MediaScanner和它的JNI

将图2-2与图2-1结合来看,能够知道:

·  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_jni.so和Native的libmedia.so交互。

从上面的分析中还可知道:

·  JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数。

下面来看MediaScanner。

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

2.2.1  Java层的MediaScanner分析

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

[-->MediaScanner.java]

public class MediaScanner

{

static{ static语句

    /*

①加载对应的JNI库,media_jni是JNI库的名字。实际加载动态库的时候会拓展成

libmedia_jni.so,在Windows平台上将拓展为media_jni.dll。

*/

       System.loadLibrary("media_jni");

       native_init();//调用native_init函数

    }

.......

//非native函数

publicvoid scanDirectories(String[] directories, String volumeName){

  ......

}

 

//②声明一个native函数。native为Java的关键字,表示它将由JNI层完成。

privatestatic native final void native_init();

    ......

privatenative void processFile(String path, String mimeType,

 MediaScannerClient client);

    ......

}

·  上面代码中列出了两个比较重要的要点:

1. 加载JNI库

前面说过,如Java要调用Native函数,就必须经过一个位于JNI层的动态库才能作到。顾名思义,动态库就是运行时加载的库,那么是何时,在什么地方加载这个库呢?

这个问题没有标准答案,原则上是在调用native函数前,任什么时候候、任何地方加载均可以。通行的作法是,在类的static语句中加载,经过调用System.loadLibrary方法就能够了。这一点,在上面的代码中也见到了,咱们之后就按这种方法编写代码便可。另外,System.loadLibrary函数的参数是动态库的名字,即media_jni。系统会自动根据不一样的平台拓展成真实的动态库文件名,例如在Linux系统上会拓展成libmedia_jni.so,而在Windows平台上则会拓展成media_jni.dll。

解决了JNI库加载的问题,再来来看第二个关键点。

2.  Java的native函数和总结

从上面代码中能够发现,native_init和processFile函数前都有Java的关键字native,它表示这两个函数将由JNI层来实现。

Java层的分析到此结束。JNI技术也很照顾Java程序员,只要完成下面两项工做就可使用JNI了,它们是:

·  加载对应的JNI库。

·  声明由关键字native修饰的函数。

因此对于Java程序员来讲,使用JNI技术真的是太容易了。不过JNI层可没这么轻松,下面来看MS的JNI层分析。

2.2.2  JNI层的MediaScanner分析

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

[-->android_media_MediaScanner.cpp]

//①这个函数是native_init的JNI层实现。

static void android_media_MediaScanner_native_init(JNIEnv *env)

{

    jclass clazz;

 

    clazz= env->FindClass("android/media/MediaScanner");

    ......

   fields.context = env->GetFieldID(clazz, "mNativeContext","I");

......

return;

}

 

//这个函数是processFile的JNI层实现。

static void android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,

jstring path, jstring mimeType, jobject client)

{

    MediaScanner*mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

    ......

    constchar *pathStr = env->GetStringUTFChars(path, NULL);

    ......

    if(mimeType) {

       env->ReleaseStringUTFChars(mimeType, mimeTypeStr);

    }

}

上面是MS的JNI层代码,不知道读者看了之后是否会产生些疑惑?

我想,最大的疑惑多是,怎么会知道Java层的native_init函数对应的是JNI层的android_media_MediaScanner_native_init函数呢?下面就来回答这个问题。

1.   注册JNI函数

正如代码中注释的那样,native_init函数对应的JNI函数是android_media_MediaScanner_native_init,但是细心的读者可能要问了,你怎么知道native_init函数对应的是这个android_media_MediaScanner_native_init,而不是其余的呢?莫非是根据函数的名字?

你们知道,native_init函数位于android.media这个包中,它的全路径名应该是android.media.MediaScanner.native_init,而JNI层函数的名字是android_media_MediaScanner_native_init。由于在Native语言中,符号“.”有着特殊的意义,因此JNI层须要把“.”换成“_”。也就是经过这种方式,native_init找到了本身JNI层的本家兄弟android.media.MediaScanner.native_init。

上面的问题其实讨论的是JNI函数的注册问题,“注册”之意就是将Java层的native函数和JNI层对应的实现函数关联起来,有了这种关联,调用Java层的native函数时,就能顺利转到JNI层对应的函数执行了。而JNI函数的注册实际上有两种方法,下面分别作介绍。

(1)静态方法

咱们从网上找到的与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。下面,来看这种方式生成的头文件:

[-->android_media_MediaScanner.h::样例文件]

/* DO NOT EDIT THIS FILE - it is machinegenerated */

#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 JNICALLJava_android_media_MediaScanner_processFile

                   (JNIEnv *, jobject, jstring,jstring, jobject);

 

......//略去一部分注释内容

//native_init对应的JNI函数

JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init

  (JNIEnv*, jclass);

 

#ifdef __cplusplus

}

#endif

#endif

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

//Java层函数名中若是有一个”_”的话,转换成JNI后就变成了”_l”。

JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init

JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile

需解释一下,静态方法中native函数是如何找到对应的JNI函数的。其实,过程很是简单:

·  当Java层调用native_init函数时,它会从对应的JNI库Java_android_media_MediaScanner_native_linit,若是没有,就会报错。若是找到,则会为这个native_init和Java_android_media_MediaScanner_native_linit创建一个关联关系,其实就是保存JNI层函数的函数指针。之后再调用native_init函数时,直接使用这个函数指针就能够了,固然这项工做是由虚拟机完成的。

从这里能够看出,静态方法就是根据函数名来创建Java函数和JNI函数之间的关联关系的,它要求JNI层函数的名字必须遵循特定的格式。这种方法也有几个弊端,它们是:

·  须要编译全部声明了native函数的Java类,每一个生成的class文件都得用javah生成一个头文件。

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

·  初次调用native函数时要根据函数名字搜索对应的JNI层函数来创建关联关系,这样会影响运行效率。

有什么办法能够克服上面三种弊端吗?根据上面的介绍,Java native函数是经过函数指针来和JNI层函数创建关联关系的。若是直接让native函数知道JNI层对应函数的函数指针,不就万事大吉了吗?这就是下面要介绍的第二种方法:动态注册法。

(2)动态注册

既然Java native函数数和JNI函数是一一对应的,那么是否是会有一个结构来保存这种关联关系呢?答案是确定的。在JNI技术中,用来记录这种一一对应关系的,是一个叫JNINativeMethod的结构,其定义以下:

typedef struct {

   //Java中native函数的名字,不用携带包的路径。例如“native_init“。

constchar* name;    

//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。

    const char* signature;

   void*       fnPtr;  //JNI层对应函数的函数指针,注意它是void*类型。

} JNINativeMethod;

应该如何使用这个结构体呢?来看MediaScanner JNI层是如何作的,代码以下所示:

[-->android_media_MediaScanner.cpp]

//定义一个JNINativeMethod数组,其成员就是MS中全部native函数的一一对应关系。

static JNINativeMethod gMethods[] = {

    ......

{

"processFile" //Java中native函数的函数名。

//processFile的签名信息,签名信息的知识,后面再作介绍。

"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",   

 (void*)android_media_MediaScanner_processFile //JNI层对应函数指针。

},

 ......

 

{

"native_init",       

"()V",                     

(void *)android_media_MediaScanner_native_init

},

  ......

};

//注册JNINativeMethod数组

int register_android_media_MediaScanner(JNIEnv*env)

{

   //调用AndroidRuntime的registerNativeMethods函数,第二个参数代表是Java中的哪一个类

    returnAndroidRuntime::registerNativeMethods(env,

               "android/media/MediaScanner", gMethods, NELEM(gMethods));

}

AndroidRunTime类提供了一个registerNativeMethods函数来完成注册工做,下面看registerNativeMethods的实现,代码以下:

[-->AndroidRunTime.cpp]

int AndroidRuntime::registerNativeMethods(JNIEnv*env,

    constchar* className, const JNINativeMethod* gMethods, int numMethods)

{

    //调用jniRegisterNativeMethods函数完成注册

    returnjniRegisterNativeMethods(env, className, gMethods, numMethods);

}

其中jniRegisterNativeMethods是Android平台中,为了方便JNI使用而提供的一个帮助函数,其代码以下所示:

[-->JNIHelp.c]

int jniRegisterNativeMethods(JNIEnv* env, constchar* className,

                                  constJNINativeMethod* gMethods, int numMethods)

{

    jclassclazz;

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

......

//其实是调用JNIEnv的RegisterNatives函数完成注册的

    if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {

       return -1;

    }

    return0;

}

wow,好像很麻烦啊!其实动态注册的工做,只用两个函数就能完成。总结以下:

/*

env指向一个JNIEnv结构体,它很是重要,后面会讨论它。classname为对应的Java类名,因为

JNINativeMethod中使用的函数名并不是全路径名,因此要指明是哪一个类。

*/

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

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

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

因此,在本身的JNI层代码中使用这种方法,就能够完成动态注册了。这里还有一个很棘手的问题:这些动态注册的函数在何时、什么地方被谁调用呢?好了,不卖关子了,直接给出该问题的答案:

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

因此,若是想使用动态注册方法,就必需要实现JNI_OnLoad函数,只有在这个函数中,才有机会完成动态注册的工做。静态注册则没有这个要求,可我建议读者也实现这个JNI_OnLoad函数,由于有一些初始化工做是能够在这里作的。

那么,libmedia_jni.so的JNI_OnLoad函数是在哪里实现的呢?因为多媒体系统不少地方都使用了JNI,因此码农把它放到android_media_MediaPlayer.cpp中了,代码以下所示:

[-->android_media_MediaPlayer.cpp]

jint JNI_OnLoad(JavaVM* vm, void* reserved)

{

   //该函数的第一个参数类型为JavaVM,这但是虚拟机在JNI层的表明喔,每一个Java进程只有一个

  //这样的JavaVM

   JNIEnv* env = NULL;

    jintresult = -1;

 

    if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {

         gotobail;

    }

    ...... //动态注册MediaScanner的JNI函数。

    if(register_android_media_MediaScanner(env) < 0) {

        goto bail;

}

......

returnJNI_VERSION_1_4;//必须返回这个值,不然会报错。

}

JNI函数注册的内容介绍完了。下面来关注JNI技术中其余的几个重要部分。

JNI层代码中通常要包含jni.h这个头文件。Android源码中提供了一个帮助头文件JNIHelp.h,它内部其实就包含了jni.h,因此咱们在本身的代码中直接包含这个JNIHelp.h便可。

2. 数据类型转换

经过前面的分析,解决了JNI函数的注册问题。下面来研究数据类型转换的问题。

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

Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这两者的。先来看基本数据类型的转换。

(1)基本类型的转换

基本类型的转换很简单,可用表2-1表示:

表2-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位

上面列出了Java基本数据类型和JNI层数据类型对应的转换关系,很是简单。不过,应务必注意,转换成Native类型后对应数据类型的字长,例如jchar在Native语言中是16位,占两个字节,这和普通的char占一个字节的状况彻底不同。

接下来看Java引用数据类型的转换。

(2)引用数据类型的转换

引用数据类型的转换如表2-2所示:

表2-2  Java引用数据类型转换关系表

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[]

floatArray

byte[]

jbyteArray

double[]

jdoubleArray

java.lang.Throwable实例

jthrowable

 

 

由上表可知:

·  除了Java中基本数据类型的数组、Class、String和Throwable外,其他全部Java对象的数据类型在JNI中都用jobject表示。

这一点太让人惊讶了!看processFile这个函数:

//Java层processFile有三个参数。

processFile(String path, StringmimeType,MediaScannerClient client);

//JNI层对应的函数,最后三个参数和processFile的参数对应。

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,

jstring path, jstring mimeType, jobject client)

从上面这段代码中能够发现:

·  Java的String类型在JNI层对应为jstring。

·  Java的MediaScannerClient类型在JNI层对应为jobject。

若是对象类型都用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)

上面的代码,引出了下面几节的主角JNIEnv。

3. JNIEnv介绍

JNIEnv是一个和线程相关的,表明JNI环境的结构体,图2-3展现了JNIEnv的内部结构:

图2-3  JNIEnv内部结构简图

从上图可知,JNIEnv实际上就是提供了一些JNI系统函数。经过这些函数能够作到:

·  调用Java的函数。

·  操做jobject对象等不少事情。

后面小节中将具体介绍怎么使用JNIEnv中的函数。这里,先介绍一个关于JNIEnv的重要知识点。

上面提到说JNIEnv,是一个和线程有关的变量。也就是说,线程A有一个JNIEnv,线程B有一个JNIEnv。因为线程相关,因此不能在线程B中使用线程A的JNIEnv结构体。读者可能会问,JNIEnv不都是native函数转换成JNI层函数后由虚拟机传进来的吗?使用传进来的这个JNIEnv总不会错吧?是的,在这种状况下使用固然不会出错。不过当后台线程收到一个网络消息,而又须要由Native层函数主动回调Java层函数时,JNIEnv是从何而来呢?根据前面的介绍可知,咱们不能保存另一个线程的JNIEnv结构体,而后把它放到后台线程中来用。这该如何是好?

还记得前面介绍的那个JNI_OnLoad函数吗?它的第一个参数是JavaVM,它是虚拟机在JNI层的表明,代码以下所示:

//全进程只有一个JavaVM对象,因此能够保存,任何地方使用都没有问题。

jint JNI_OnLoad(JavaVM* vm, void* reserved)

正如上面代码所说,不论进程中有多少个线程,JavaVM倒是独此一份,因此在任何地方均可以使用它。那么,JavaVM和JNIEnv又有什么关系呢?答案以下:

·  调用JavaVM的AttachCurrentThread函数,就可获得这个线程的JNIEnv结构体。这样就能够在后台线程中回调Java函数了。

·  另外,后台线程退出前,须要调用JavaVM的DetachCurrentThread函数来释放对应的资源。

再来看JNIEnv的做用。

4. 经过JNIEnv操做jobject

前面提到过一个问题,即Java的引用类型除了少数几个外,最终在JNI层都用jobject来表示对象的数据类型,那么该如何操做这个jobject呢?

从另一个角度来解释这个问题。一个Java对象是由什么组成的?固然是它的成员变量和成员函数了。那么,操做jobject的本质就应当是操做这些对象的成员变量和成员函数。因此应先来看与成员变量及成员函数有关的内容。

(1)jfieldID 和jmethodID的介绍

咱们知道,成员变量和成员函数是由类定义的,它是类的属性,因此在JNI规则中,用jfieldID 和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为这个函数和变量的签名信息。如前所示,成员函数和成员变量都是类的信息,这两个函数的第一个参数都是jclass。

MS中是怎么使用它们的呢?来看代码,以下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient构造函数]

 MyMediaScannerClient(JNIEnv *env, jobjectclient)......

{

 //先找到android.media.MediaScannerClient类在JNI层中对应的jclass实例。

jclass mediaScannerClientInterface =

env->FindClass("android/media/MediaScannerClient");

 //取出MediaScannerClient类中函数scanFile的jMethodID。

mScanFileMethodID = env->GetMethodID(

mediaScannerClientInterface, "scanFile",

                           "(Ljava/lang/String;JJ)V");

 //取出MediaScannerClient类中函数handleStringTag的jMethodID。

 mHandleStringTagMethodID = env->GetMethodID(

mediaScannerClientInterface,"handleStringTag",

                             "(Ljava/lang/String;Ljava/lang/String;)V");

  ......

}

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

·  若是每次操做jobject前都去查询jmethoID或jfieldID的话将会影响程序运行的效率。因此咱们在初始化的时候,就能够取出这些ID并保存起来以供后续使用。

取出jmethodID后,又该怎么用它呢?

(2)使用jfieldID和jmethodID

下面再看一个例子,其代码以下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile]

 virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

    {

       jstring pathStr;

        if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

       

/*

调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的参数:

第一个是表明MediaScannerClient的jobject对象,

第二个参数是函数scanFile的jmethodID,后面是Java中scanFile的参数。

*/

       mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

 

       mEnv->DeleteLocalRef(pathStr);

       return (!mEnv->ExceptionCheck());

}

明白了,经过JNIEnv输出的CallVoidMethod,再把jobject、jMethodID和对应参数传进去,JNI层就可以调用Java对象的函数了!

实际上JNIEnv输出了一系列相似CallVoidMethod的函数,形式以下:

NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)。

其中type是对应Java函数的返回值类型,例如CallIntMethod、CallVoidMethod等。

上面是针对非static函数的,若是想调用Java中的static函数,则用JNIEnv输出的CallStatic<Type>Method系列函数。

如今,咱们已了解了如何经过JNIEnv操做jobject的成员函数,那么怎么经过jfieldID操做jobject的成员变量呢?这里,直接给出总体解决方案,以下所示:

//得到fieldID后,可调用Get<type>Field系列函数获取jobject对应成员变量的值。

NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)

//或者调用Set<type>Field系列函数来设置jobject对应成员变量的值。

void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

//下面咱们列出一些参加的Get/Set函数。

GetObjectField()         SetObjectField()

GetBooleanField()         SetBooleanField()

GetByteField()           SetByteField()

GetCharField()           SetCharField()

GetShortField()          SetShortField()

GetIntField()            SetIntField()

GetLongField()           SetLongField()

GetFloatField()          SetFloatField()

GetDoubleField()                  SetDoubleField()

经过本节的介绍,相信读者已了解jfieldID和jmethodID的做用,也知道如何经过JNIEnv的函数来操做jobject了。虽然jobject是透明的,但有了JNIEnv的帮助,仍是能轻松操做jobject背后的实际对象了。

5. jstring介绍

Java中的String也是引用类型,不过因为它的使用很是频繁,因此在JNI规范中单首创建了一个jstring类型来表示Java中的String类型。虽然jstring是一种独立的数据类型,可是它并无提供成员函数供操做。相比而言,C++中的string类就有本身的成员函数了。那么该怎么操做jstring呢?仍是得依靠JNIEnv提供的帮助。这里看几个有关jstring的函数:

·  调用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),能够从Native的字符串获得一个jstring对象。其实,能够把一个jstring对象当作是Java中String对象在JNI层的表明,也就是说,jstring就是一个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的内部实现有关系,读者写代码时务必注意这个问题。

为了加深印象,来看processFile是怎么作的:

[-->android_media_MediaScanner.cpp]

static void

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

{

   MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz,fields.context);

......

//调用JNIEnv的GetStringUTFChars获得本地字符串pathStr

    constchar *pathStr = env->GetStringUTFChars(path, NULL);

......

//使用完后,必须调用ReleaseStringUTFChars释放资源

   env->ReleaseStringUTFChars(path, pathStr);

    ......

}

6. JNI类型签名的介绍

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

tatic JNINativeMethod gMethods[] = {

    ......

{

"processFile"

//processFile的签名信息,这么长的字符串,是什么意思?

"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",   

 (void*)android_media_MediaScanner_processFile

},

  ......

}

上面代码中的JNINativeMethod已经见过了,不过其中那个很长的字符串"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V"是什么意思呢?

根据前面的介绍可知,它是Java中对应函数的签名信息,由参数类型和返回值类型共同组成。不过为何须要这个签名信息呢?

·  这个问题的答案比较简单。由于Java支持函数重载,也就是说,能够定义同名但不一样参数的函数。但仅仅根据函数名,是无法找到具体函数的。为了解决这个问题,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类型。

函数签名不只看起来麻烦,写起来更麻烦,稍微写错一个标点就会致使注册失败。因此,在具体编码时,读者能够定义字符串宏,这样改起来也方便。

表2-3是常见的类型标示:

表2-3  类型标示示意表

类型标示

Java类型

类型标示

Java类型

Z

boolean

F

float

B

byte

D

double

C

char

L/java/langaugeString;

String

S

short

[I

int[]

I

int

[L/java/lang/object;

Object[]

J

long

 

 

上面列出了一些经常使用的类型标示。请读者注意,若是Java类型是数组,则标示中会有一个“[”,另外,引用类型(除基本类型的数组外)的标示最后都有一个“;”。

再来看一个小例子,如表2-4所示:

表2-4  函数签名小例子

函数签名

Java函数

“()Ljava/lang/String;”

String f()

“(ILjava/lang/Class;)J”

long f(int i, Class c)

“([B)V”

void f(byte[] bytes)

请读者结合表2-3和表2-4左栏的内容写出对应的Java函数。

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

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

有了javap,就不用死记硬背上面的类型标示了。

7. 垃圾回收

咱们知道,Java中建立的对象最后是由垃圾回收器来回收和释放内存的,可它对JNI有什么影响呢?下面看一个例子:

[-->垃圾回收例子]

static jobject save_thiz = NULL; //定义一个全局的jobject

static void

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path,

 jstringmimeType, jobject client)

{

  ......

  //保存Java层传入的jobject对象,表明MediaScanner对象

save_thiz = thiz;

......

return;

}

//假设在某个时间,有地方调用callMediaScanner函数

void callMediaScanner()

{

  //在这个函数中操做save_thiz,会有问题吗?

}

上面的作法确定会有问题,由于和save_thiz对应的Java层中的MediaScanner颇有可能已经被垃圾回收了,也就是说,save_thiz保存的这个jobject多是一个野指针,如使用它,后果会很严重。

可能有人要问,将一个引用类型进行赋值操做,它的引用计数不会增长吗?而垃圾回收机制只会保证那些没有被引用的对象才会被清理。问得对,但若是在JNI层使用下面这样的语句,是不会增长引用计数的。

save_thiz = thiz; //这种赋值不会增长jobject的引用计数。

那该怎么办?没必要担忧,JNI规范已很好地解决了这一问题,JNI技术一共提供了三种类型的引用,它们分别是:

·  Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject、在JNI层函数中建立的jobject。LocalReference最大的特色就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。

·  Global Reference:全局引用,这种对象如不主动释放,就永远不会被垃圾回收。

·  Weak Global Reference:弱全局引用,一种特殊的GlobalReference,在运行过程当中可能会被垃圾回收。因此在程序中使用它以前,须要调用JNIEnv的IsSameObject判断它是否是被回收了。

平时用得最多的是Local Reference和Global Reference,下面看一个实例,代码以下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient构造函数]

 MyMediaScannerClient(JNIEnv *env, jobjectclient)

       :   mEnv(env),

        //调用NewGlobalRef建立一个GlobalReference,这样mClient就不用担忧被回收了。

           mClient(env->NewGlobalRef(client)),

           mScanFileMethodID(0),

           mHandleStringTagMethodID(0),

           mSetMimeTypeMethodID(0)

{

  ......

}

//析构函数

virtual ~MyMediaScannerClient()

{

  mEnv->DeleteGlobalRef(mClient);//调用DeleteGlobalRef释放这个全局引用。

 }

每当JNI层想要保存Java层中的某个对象时,就可使用Global Reference,使用完后记住释放它就能够了。这一点很容易理解。下面要讲有关LocalReference的一个问题,仍是先看实例,代码以下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile]

 virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

{

   jstringpathStr;

   //调用NewStringUTF建立一个jstring对象,它是Local Reference类型。

   if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

        //调用Java的scanFile函数,把这个jstring传进去

       mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

     /*

      根据LocalReference的说明,这个函数返回后,pathStr对象就会被回收。因此

      下面这个DeleteLocalRef调用看起来是多余的,其实否则,这里解释一下缘由:

1)若是不调用DeleteLocalRef,pathStr将在函数返回后被回收。

2)若是调用DeleteLocalRef的话,pathStr会当即被回收。这二者看起来没什么区别,

不过代码要是像下面这样的话,虚拟机的内存就会被很快被耗尽:

      for(inti = 0; i < 100; i++)

      {

           jstring pathStr = mEnv->NewStringUTF(path);

           ......//作一些操做

          //mEnv->DeleteLocalRef(pathStr); //不当即释放Local Reference

}

若是在上面代码的循环中不调用DeleteLocalRef的话,则会建立100个jstring,

那么内存的耗费就很是可观了!

     */

   mEnv->DeleteLocalRef(pathStr);

   return(!mEnv->ExceptionCheck());

}

因此,没有及时回收的Local Reference或许是进程占用过多的一个缘由,请务必注意这一点。

8. JNI中的异常处理

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

来看一个和异常处理有关的例子,代码以下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile函数]

 virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

 {

       jstring pathStr;

       //NewStringUTF调用失败后,直接返回,不能再干别的事情了。

        if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

       ......

}

JNI层函数能够在代码中截获和修改这些异常,JNIEnv提供了三个函数进行帮助:

·  ExceptionOccured函数,用来判断是否发生异常。

·  ExceptionClear函数,用来清理当前JNI层中发生的异常。

·  ThrowNew函数,用来向Java层抛出异常。

异常处理是JNI层代码必须关注的事情,读者在编写代码时务当心对待。

2.3  本章小结

本章经过一个实例介绍了JNI技术中的几个重要方面,包括:

·  JNI函数注册的方法。

·  Java和JNI层数据类型的转换。

·  JNIEnv和jstring的使用方法,以及JNI中的类型签名。

·  最后介绍了垃圾回收在JNI层中的使用,以及异常处理方面的知识。

相信掌握了上面的知识后,咱们会对JNI技术有一个比较清晰的认识。这里,还要建议读者再认真阅读一下JDK文档中的《Java Native Interface Specification》,它完整和细致地阐述了JNI技术的各个方面,堪称深刻学习JNI的权威指南。

相关文章
相关标签/搜索