9/5/2016 2:10:30 PMjava
本章涉及的源代码文件名及位置android
AndroidRunTime.cpp (framework/base/core/jni/AndroidRunTime.cpp) JNIHelp.c (libnativehelper/JNIHelp.c)
源码的下载可参考上一篇博客:android源码-下载与管理程序员
JNI,是Java Native Interface的缩写,中文为Java本地调用。通俗地说,JNI是一种技术,经过这种技术能够作到如下两点:数组
在Android平台上,JNI就是一座将Native世界和Java世界间的天堑变成通途的桥,它展现了Android平台上JNI所处的位置:网络
由上图可知,JNI将Java世界和Native世界紧密地联系在一块儿了。在Android平台上尽情使用Java开发的程序员们不要忘了,若是没有JNI的支持,咱们将步履维艰!函数
Java世界与Native世界经过JNI互通,Java世界与JNI之间创建直接互统统道,主要须要完成了两个比较重要的要点工具
(1) 加载JNI库编码
Java要调用Native函数,就必须经过一个位于JNI层的动态库才能作到。顾名思义,动态库就是运行时加载的库,那么是何时,在什么地方加载这个库呢?.net
这个问题没有标准答案,原则上是在调用native函数前,任什么时候候、任何地方加载均可以。通行的作法是,在类的static语句中加载,经过调用System.loadLibrary方法就能够了。System.loadLibrary函数的参数是动态库的名字,即xxx_jni
。系统会自动根据不一样的平台拓展成真实的动态拓展名,例如在Linux系统上会拓展成xxx_jni.so
,而在Windows平台上则会拓展成xxx_jni.dll
。线程
(2) Java的native函数调用
Java函数前都有Java的关键字native,它表示这两个函数将由JNI层来实现。 因此对于Java程序员来讲,使用JNI技术真的是太容易了。不过JNI层可没这么轻松,下面来看MS的JNI层分析。
对于JNI桥接来说,最大的疑惑多是,怎么会知道Java层的native_xx函数对应的是JNI层的什么函数呢,对应关系是什么?
上面的问题其实讨论的是JNI函数的注册问题,“注册”之意就是将Java层的native函数和JNI层对应的实现函数关联起来,有了这种关联,调用Java层的native函数时,就能顺利转到JNI层对应的函数执行了。而JNI函数的注册实际上有两种方法,下面分别作介绍。
(1) 静态方法调用
咱们从网上找到的与JNI有的关资料,通常都会介绍如何使用这种方法完成JNI函数的注册,这种方法就是根据函数名来找对应的JNI函数。这种方法须要Java的工具程序javah参与,总体流程以下:
这个头文件的名字通常都会使用packagename_class.h的样式。
javah com.xio.HelloWorld
获得以下声明:
#include <jni.h> #ifndef _Included_com_xio_HelloWorld #define _Included_com_xio_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: com_xio_HelloWorld * Method: printJNI * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_xio_HelloWorld_print_ljni (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
从上面代码中能够发现,printJNI的JNI层函数被声明成:
// Java层函数名中若是有一个”_”的话,转换成JNI后就变成了”_l”。 JNIEXPORT jstring JNICALL Java_com_xio_HelloWorld_print_ljni
需解释一下,静态方法中native函数是如何找到对应的JNI函数的。其实,过程很是简单:
print_jni
函数时,它会从对应的JNI库Java_com_xio_HelloWorld_print_ljni
,若是没有,就会报错。若是找到,则会为这个print_jni
和Java_com_xio_HelloWorld_print_ljni
创建一个关联关系,其实就是保存JNI层函数的函数指针。之后再调用print_jni
函数时,直接使用这个函数指针就能够了,固然这项工做是由虚拟机完成的。从这里能够看出,静态方法就是根据函数名来创建Java函数和JNI函数之间的关联关系的,它要求JNI层函数的名字必须遵循特定的格式。这种方法也有几个弊端,它们是:
有什么办法能够克服上面三种弊端吗?根据上面的介绍,Java native函数是经过函数指针来和JNI层函数创建关联关系的。若是直接让native函数知道JNI层对应函数的函数指针,不就万事大吉了吗?这就是下面要介绍的第二种方法:动态注册法。
(2) 动态注册
既然Java native函数数和JNI函数是一一对应的,那么是否是会有一个结构来保存这种关联关系呢?答案是确定的。在JNI技术中,用来记录这种一一对应关系的,是一个叫JNINativeMethod的结构,其定义以下:
typedef struct { constchar* name; const char* signature; void* fnPtr; } JNINativeMethod; // 第一个变量name是Java中函数的名字。 // 第二个变量signature,用字符串是描述了Java中函数的参数和返回值 // 第三个变量fnPtr是函数指针,指向native函数。前面都要接 (void *)
应该如何使用这个结构体呢?来看JNI层是如何作的,代码以下所示:
//定义一个JNINativeMethod数组,其成员就是MS中全部native函数的一一对应关系。
static JNINativeMethod gMethods[] = { // Java中native函数的函数名。 "print_jni" //print_jni的签名信息 "()V", //JNI层对应函数指针。 (void*)com_xio_HelloWorld_printJni }; int com_xio_printJni(JNIEnv*env) { //调用AndroidRuntime的registerNativeMethods函数 returnAndroidRuntime::registerNativeMethods(env, "com/xio/HelloWorld", 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; }
其实动态注册的工做,只用两个函数就能完成。总结以下:
jclass clazz = (*env)->FindClass(env, className); //调用JNIEnv的RegisterNatives函数,注册关联关系。 (*env)->RegisterNatives(env, clazz, gMethods,numMethods);
当Java层经过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad
的函数,若是有,就调用它,而动态注册的工做就是在这里完成的。
因此,若是想使用动态注册方法,就必需要实现JNI_OnLoad函数,只有在这个函数中,才有机会完成动态注册的工做。静态注册则没有这个要求,可我建议读者也实现这个JNI_OnLoad函数,由于有一些初始化工做是能够在这里作的。
jint JNI_OnLoad(JavaVM* vm, void* reserved) { //该函数的第一个参数类型为JavaVM,这但是虚拟机在JNI层的表明喔,每一个Java进程只有一个 JNIEnv* env = NULL; if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { goto bail; } assert(env != NULL); if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) { goto bail; } return JNI_VERSION_1_4;//必须返回这个值,不然会报错。 }
JNI函数注册的内容介绍完了。
注意:JNI层代码中通常要包含jni.h这个头文件。Android源码中提供了一个帮助头文件JNIHelp.h,它内部其实就包含了jni.h,因此咱们在本身的代码中直接包含这个JNIHelp.h便可。
经过前面的分析,解决了JNI函数的注册问题。下面来研究数据类型转换的问题。在Java中调用native函数传递的参数是Java数据类型,那么这些参数类型到了JNI层会变成什么呢?
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这两者的。先来看基本数据类型的转换。
基本类型的转换很简单,可用表1表示:
表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 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表示。这一点太让人惊讶了!
JNIEnv是一个和线程相关的,表明JNI环境的结构体,如图展现了JNIEnv的内部结构:
从上图可知,JNIEnv实际上就是提供了一些JNI系统函数。经过这些函数能够作到:
这里,先介绍一个关于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又有什么关系呢?答案以下:
再来看JNIEnv的做用。
前面提到过一个问题,即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。
在上面代码中,将scanFile和handleStringTag函数的jmethodID保存为MyMediaScannerClient的成员变量。为何这里要把它们保存起来呢?这个问题涉及一个事关程序运行效率的知识点:
· 若是每次操做jobject前都去查询jmethoID或jfieldID的话将会影响程序运行的效率。因此咱们在初始化的时候,就能够取出这些ID并保存起来以供后续使用。
取出jmethodID后,又该怎么用它呢?
(2) 使用jfieldID和jmethodID
经过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函数
|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背后的实际对象了。
Java中的String也是引用类型,不过因为它的使用很是频繁,因此在JNI规范中单首创建了一个jstring类型来表示Java中的String类型。虽然jstring是一种独立的数据类型,可是它并无提供成员函数供操做。相比而言,C++中的string类就有本身的成员函数了。那么该怎么操做jstring呢?仍是得依靠JNIEnv提供的帮助。这里看几个有关jstring的函数:
调用JNIEnv的NewString(JNIEnv env, const jcharunicodeChars,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的内部实现有关系,读者写代码时务必注意这个问题。
先来看动态注册中的一段代码:
static JNINativeMethod gMethods[] = { // Java中native函数的函数名。 "print_jni" //print_jni的签名信息 "()V", //JNI层对应函数指针。 (void*)com_xio_HelloWorld_printJni };
上面代码中的JNINativeMethod已经见过了,不过其中那个字符串"()V",根据前面的介绍可知,它是Java中对应函数的签名信息,由参数类型和返回值类型共同组成。不过为何须要这个签名信息呢?
JNI规范定义的函数签名信息看起来很别扭,不过习惯就行了。它的格式是:
(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示。
当参数的类型是引用类型时,其格式是”L包名;”,其中包名中的”.”换成”/”。上面例子中的
Ljava/lang/String;表示是一个Java String类型。
函数签名不只看起来麻烦,写起来更麻烦,稍微写错一个标点就会致使注册失败。因此,在具体编码时,读者能够定义字符串宏,这样改起来也方便。
表1 类型标示示意表
|类型标示 |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所示:
表2 函数签名小例子
|函数签名 |Java函数 | |--| |“()Ljava/lang/String;” |String f() | |“(ILjava/lang/Class;)J”|long f(int i, Class c)| |“([B)V” |void f(byte[] bytes)|
虽然函数签名信息很容易写错,但Java提供一个叫javap的工具能帮助生成函数或变量的签名信息,它的用法以下:
javap –s -p xxx。其中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印全部函数和成员的签名信息,而默认只会打印public成员和函数的签名信息。
有了javap,就不用死记硬背上面的类型标示了。
咱们知道,Java中建立的对象最后是由垃圾回收器来回收和释放内存的,可它对JNI有什么影响呢?下面看一个例子:
[-->垃圾回收例子]
static jobject save_thiz = NULL; //定义一个全局的jobject static void com_xio_HelloWorld_print_jni(JNIEnv*env, jobject thiz, jstring path, jstringmimeType, jobject client) { //保存Java层传入的jobject对象,表明MediaScanner对象 save_thiz = thiz; return; } //假设在某个时间,有地方调用callMediaScanner函数 void function1() { //在这个函数中操做save_thiz,会有问题吗? }
上面的作法确定会有问题,由于和save_thiz对应的Java层中的对象颇有可能已经被垃圾回收了,也就是说,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,下面看一个实例,代码以下所示:
xxClient(JNIEnv *env, jobjectclient) :mEnv(env), //调用NewGlobalRef建立一个GlobalReference,这样mClient就不用担忧被回收了。 mClient(env->NewGlobalRef(client)) { } //析构函数 virtual ~MyMediaScannerClient() { mEnv->DeleteGlobalRef(mClient);//调用DeleteGlobalRef释放这个全局引用。 }
每当JNI层想要保存Java层中的某个对象时,就可使用Global Reference,使用完后记住释放它就能够了。
由于Local Reference得回收并不是是及时的,若是没有及时回收的Local Reference或许是进程占用过多的一个缘由,请务必注意这一点。
JNI中也有异常,不过它和C++、Java的异常不太同样。当调用JNIEnv的某些函数出错后,会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能作一些资源清理工做了(例如释放全局引用,或者ReleaseStringChars)。若是这时调用除上面所说函数以外的其余JNIEnv函数,则会致使程序死掉。
JNI层函数能够在代码中截获和修改这些异常,JNIEnv提供了三个函数进行帮助:
ExceptionOccured函数,用来判断是否发生异常。
ExceptionClear函数,用来清理当前JNI层中发生的异常。
ThrowNew函数,用来向Java层抛出异常。
异常处理是JNI层代码必须关注的事情,读者在编写代码时务当心对待。
本章经过一个实例介绍了JNI技术中的几个重要方面,包括:
java层桥接JNI函数注册的方法。
Native如何与JNI创建联系
Java和JNI层数据类型的转换。
JNIEnv和jstring的使用方法,以及JNI中的类型签名。
最后介绍了垃圾回收在JNI层中的使用,以及异常处理方面的知识。
##参考文献