深刻理解JNI

深刻理解JNI

本章主要内容html

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

本章涉及的源代码文件名称及位置android

如下是本章分析的源代码文件名称及其位置。数据库

·  MediaScanner.java数组

framework/base/media/java/src/android/media/MediaScanner.java网络

·  android_media_MediaScanner.cpp函数

framework/base/media/jni/MediaScanner.cpp工具

·  android_media_MediaPlayer.cpppost

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的keyword,表示它将由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的keywordnative,它表示这两个函数将由JNI层来实现。

Java层的分析到此结束。

JNI技术也很是照应Java程序猿。仅仅要完毕如下两项工做就可以使用JNI了,它们是:

·  载入相应的JNI库。

·  声明由keywordnative修饰的函数。

因此对于Java程序猿来讲。使用JNI技术真的是太easy了。只是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函数的。事实上。过程很是easy:

·  当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)基本类型的转换

基本类型的转换很是easy。可用表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层数据类型相应的转换关系。很是easy。只是,应务必注意,转换成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函数。

尽管函数签名信息很是easy写错。但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,使用完后记住释放它就可以了。这一点很是easy理解。如下要讲有关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的权威指南。

相关文章
相关标签/搜索