JNI(Java Native Interface)Java本地接口,它的存在是为了:
- java程序中的函数可以调用Native语言编写的函数,一般是C/C++
- 本地函数(C/C++编写的函数)可以调用Java层的函数。
也就是说JNI提供了底层语言与上层java之间交互的通道。
那么JNI是如何实现这一点的呢?
首先,既然java中可以调用Native的函数,那么就说明,在Java的世界和Native的世界中,包含相互对应的方法与函数,那么它们之间是如何对应的呢?先看下图:
以MediaScanner为例,在Java中对应的类是MediaScanner,Native层与之对应的是libmedia.so,这个库文件完成了实际的功能。JNI作为连接对应的是libmedia_jni.so。media_jni是JNI库的名字,其中下划线前的media是Native层库的名字,就是这里的libmedia库,下划线之后的jni表示它是一个JNI库。
注:JNI库的名字是可以随便取的,但是android中基本都在用“lib模块名_jni.so”格式命名。
由此可知,在JNI层必须用动态库的形式实现,这样虚拟机才能加载它并调用它的函数。
当然,并不是通过这种命名规则JNI就知道如何来对应我们的Java层和Native层的方法与函数,这需要注册JNI函数,再如何注册之前,先看下在java层如何调用JNI。
还是以MediaScanner为例:
public class MediaScanner { static { System.loadLibrary("media_jni"); native_init(); }
在static语句中,首先通过System.loadLibrary()来加载JNI库,其中的media_jni是JNI库的名字,在实际加载动态库的时候,会将其扩展成libmedia_jni.so。在window平台上则扩展为meida_jni.dll。然后调用native_init函数。
private static native final void native_init(); private native void processFile(String path, String mimeType, MediaScannerClient client);
这里使用native关键字声明了函数,标明这些函数,由JNI层来完成。
由以上信息可以看出,在Java层调用JNI层函数只有两个步骤:加载JNI库和声明Java中的native函数。
那么在JNI层是如何关联java方法与JNI层中的实现的呢?首先看看JNI层是如何实现我们在Java层用Native关键字声明的方法的:
我们看到在java中的方法processFile(),对应在JNI层是android_media_MediaScanner_processFile。那么系统是如何做到让这两者匹配的呢?这就是我们之前说的java注册。注册之后,Java层的native函数就和JNI层的实现函数关联起来了,有了这种关联,当调用Java层native函数是,就能顺利的转移到JNI层来实现了。注册JNI函数有两种方式:静态注册和动态注册。
静态注册,这种方式就是根据函数名来找对应JNI函数,需要Java的工具程序javah参与,流程如下:
1.先编写java代码,然后编译生成.class文件。
2.使用javah,如javah –o output packagename.classname,这样就会生成一个output.h的JNI层头文件,在这个文件里,对应了JNI层的函数,只要实现里面的函数即可。
这个头文件一般都会使用packagename_class.h的样式,例如MediaScanner对应的JNI层头文件为android_media_MediaScanner.h。内容如下:
总结下:静态注册其实比较简单,当java层调用processFile函数时,它会从对应的JNI库中寻找java_android_media_MediaScanner_processFile函数,如果没有则报错,如果有,则为两者建立一个对应关系,其实也就是保存一个JNI层函数的指针,当以后再次调用processFile函数时,就会直接使用这个指针。
但是,这种方式也有一定的弊端:
1.需要声明所有声明了native关键字函数的java类,每个生成的class文件都的用javah生成头文件。
2.javah生成的JNI层函数名特别长,不方便书写,使用。
3.初次使用时,要根据名字搜索对应JNI层函数建立关系,这样影响运行效率。
动态注册,既然我们说Java层native函数与JNI层函数有一一对应的关系,那么可否利用一种结构来保存这种对应关系呢?动态注册也就由此出发,利用一个叫JNINativeMethod结构来保存这种一一对应关系,定义如下:
typedf struct{ const char* name; //java中native函数名,processFile const char* signature; //java函数的签名信息,字符串表示参数类型与返回值类型 void* fnPtr; //JNI层对应函数的函数指针,类型为void* }JNINativeMethod;
那么如何使用这种结构体呢?我们看JNI层是如何做的:
static JNINativeMethod gMethods[]={ …… { "processFile"//Java中native的函数名 //porcessFile的签名信息 "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V”, //JNI层对应的函数指针 (void *)android_media_mediaScanner_processFile }, …… }; …… int register_android_media_MediaScanner(JNIEnv *env){ //调用AndroidRuntime的registerNativeMethods函数,第二个参数标明是Java中的哪个表 return AndroidRuntime::registerNativeMethods(env, "android/media/Media/MediaScanner",gMethods,NELEM(gMethods) } AndroidRuntime类提供了一个registerNativeMethods()方法来完成注册,在registerNativeMethods中: 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); …… if((*env)->RegisterNatives(env,clazz,gMethods,numMethods)<0){ return -1; } return 0; }
总结如下:其实动态注册只做了两步:
1.使用env指向一个JNIEnv结构体,className对应Java类名,由于JNINativeMethod中使用的函数名不是全路径,因此要指明是哪个类。
2.调用JNIEnv的RegisterNatives函数完成注册。
那么JNI中的注册信息时在什么时候和什么地方被调用的呢?
当Java层调用System.loadLibrary加载JNI动态库之后,紧接着会查找该库中一个JNI_OnLoad的函数。如果有,则调用它,动态注册就是在这里完成的。
在静态注册中对改函数没有要求,但是动态注册则必须有。
前面总结了JNI是如何和Java层建立关联,如何在程序中注册的,下面主要说下JNI的类型以及签名。
JNI的数据类型
在Java中的函数类型有基本数据类型和引用数据类型。那么这些类型在JNI层中会变成什么呢?下表是JNI层数据转换:
引用数据类型转换:
Java引用类型 |
Native类型 |
Java引用类型 |
Native类型 |
All objects |
jobjects |
char[] |
jcharArray |
Java.long.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 |
boolean |
byte |
char |
short |
int |
long |
float |
double |
Native |
jboolean |
jbyte |
jchar |
jshort |
jint |
jlong |
jfloat |
jdouble |
由以上两表,我们可以看出来除了基本数据类型,基本类型数组,Class,String,以及Throwable之外的所有Java对象的数据类型,在JNI中都用jobject来表示,这就好比Native层void*一样。
JNI类型签名
我们之前在动态注册中有一段代码:
static JNINativeMethod gMethods[]={ …… { "processFile"//Java中native的函数名 //porcessFile的签名信息 "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V”, //JNI层对应的函数指针 (void *)android_media_mediaScanner_processFile }, …… };
中间的这一段代码的含义是什么呢:
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V”,
这段代码表示java中对应函数的签名信息,包含了参数类型和返回值类型。JNI中这么做是由于,在java中支持函数的重载,也就是函数命可以相同,但是函数的参数个数及类型不同,因此仅仅通过函数名是不能找到具体函数的,因此JNI中就将参数类型和返回值类型共同组成一个签名信息,有了签名信息和函数名,就能够找到java中的函数了。
那么签名的格式规定为:
(参数1类型标示;参数2类型标示……;参数n类型标示)返回类型标示
因此 void processFile(String path,String mimeType,MediaScannerClient client)对应的JNI函数签名就为:
(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V
当参数的类型为引用类型是,格式为“L包名”,包名中的“.”换成“/”即可。
因此上面的Ljava/long/String就标示java.long.String类型(String类型)最右边的V表示返回值类型,void类型的对应标示为V.常见的标示见下表:
类型标示 |
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中有提供一个叫Javap的工具能帮助生成签名信息,格式为:javap –s –p ****;其中****味编译后的class文件,s标示输出内部数据类型的签名信息,p标示打印出所有public类型的函数和成员签名信息