目录
1 JNI概述
JNI是Java Native Interface的缩写,中文译为“Java本地调用”。通俗地说,JNI是一种技术,通过这种技术可以做到以下两点:
a). Java程序中的函数可以调用Native语言写的函数,
Native一般指的是C/C++编写的函数
。
b). Native程序中的函数可以调用Java层的函数,也就是说在C/C++程序中可以调用Java的函数。
也就是
Java与C/C++代码互调
在平台无关的Java中,为什么要创建一个与Native相关的JNI技术呢?这岂不是破坏了Java的平台无关特性吗?JNI技术的推出有以下几个方面的考虑:
承载Java世界的虚拟机是用Native语言写的,而虚拟机又运行在具体的平台上,所以虚拟机本身无法做到平台无关。然而,有了JNI技术后就可以对Java层屏蔽不同操作系统平台(如Windows和Linux)之间的差异了(例如同样是打开一个文件,Windows上的API使用OpenFile函数,而Linux上的API是open函数)。这样,就能实现Java本身的平台无关特性。其实Java一直在使用JNI技术,只是我们平时较少用到罢了。
早在Java语言诞生前,很多程序都是用C/C++语言写的,它们遍布在软件世界的各个角落。Java出世后,它受到了追捧,并迅速得到发展,但仍无法将软件世界彻底改朝换代,于是才有了折中的办法。既然已经有C/C++模块实现了相关功能,那么在Java中通过JNI技术直接使用它们就行了,免得落下重复制造轮子的坏名声。另外,在一些要求效率和速度的场合还是需要C/C++语言参与的。
在Android平台上,JNI就是一座将C/C++世界和Java世界间的天堑变成通途的桥。下图展示了Android平台上JNI所处的位置:
2 学习JNI的实例:MediaPlayer
初次接触JNI,感觉最神奇的就是Java竟然能够调用Native的函数,可它是怎么做到的呢?由于Android大量使用了JNI技术,本章接下来的内容就将通过源码中的一处实例来讲解JNI相关的知识。
其中这个例子是和MediaPlayer相关的,本书的最后一章会详细分析它的工作原理,这里先看与JNI相关的部分,如下图所示:
结合来看,可以知道:
Java世界对应的是MediaPlayer,而这个MediaPlayer类有一些函数需要由Native层来实现。JNI层对应的是libmedia_jni.so。media_jni是JNI库的名字,其中,
下划线前的“media”是Native层库的名字
,这里就是libmedia库。
下划线后的“jni”表示它是一个JNI库
。注意,JNI库的名字可以随便取,不过
Android平台基本上都采用“lib模块名_jni.so”的命名方式
。
Native层对应的是libmedia.so,这个库完成了实际的功能。
MediaPlayer将通过JNI库libmedia_jni.so和Native层的libmedia.so交互。
从上面的分析中还可知道:
JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数
。
上面代码中列出了两个比较重要的要点:
一个是加载JNI库;另一个是Java的native函数
。
2.1 加载JNI库
前面说过,如果Java要调用native函数,就必须通过一个位于JNI层的动态库来实现。顾名思义,动态库就是运行时加载的库,那么在什么时候以及什么地方加载这个库呢?
这个问题没有标准答案,
原则上是:在调用native函数前,任何时候、任何地方加载都可以
。
通行的做法是在类的static语句中加载,调用System.loadLibrary方法就可以了
。这一点在上面的代码中也见到了,我们以后就按这种方法编写代码即可。另外,System.loadLibrary函数的参数是动态库的名字,即media_jni。系统会自动根据不同的平台拓展成真实的动态库文件名,例如在Linux系统上会拓展成libmedia_jni.so,而在Windows平台上则会拓展成media_jni.dll。
从上面代码中可以发现,native_init函数前有Java的关键字
native,它表示这个函数将由JNI层来实现
。
2.2 JNI层MediaPlayer的分析
上面是JNI层代码,不知道大家看了以后是否会产生一些疑惑?最大的疑惑可能是,如何才能知道
Java层的native_init函数对应的是JNI层的android_media_MediaPlayer_native_init(JNIEnv *env)函数
呢?
2.3 注册JNI函数
native_init函数位于android.media这个包中,它的
全路径名
应该是android.media.MediaPlayer.native_init,而JNI层函数的名字是android_media_MediaPlayer_native_init。因为在Native语言中,符号“.”有着特殊的意义,所以
JNI层需要把Java函数名称(包括包名)中的“.”换成“_”
。也就是通过这种方式,native_init找到了自己JNI层的本家兄弟android.media.MediaPlayer.native_init。
“注册”之意就是将Java层的native函数和JNI层对应的实现函数关联起来,有了这种关联,调用Java层的native函数时,就能顺利转到JNI层对应的函数执行了。而JNI函数的注册方法实际上有两种,下面分别做介绍。
2.3.1 静态注册
//Todo…
当Java层调用native_init函数时,它会从对应的JNI库中寻找Java_android_media_MediaPlayer_native_init函数,如果没有,就会报错。如果找到,则会为这个native_init和android_media_MediaPlayer_native_init建立一个关联关系,其实就是
保存JNI层函数的函数指针
。以后再调用native_init函数时,直接使用这个函数指针就可以了,当然
这项工作是由虚拟机完成的
。
静态方法就是
根据函数名来建立Java函数和JNI函数之间的关联关系的
,而且它要求JNI层函数的名字必须遵循特定的格式
。这种方法也有几个弊端,即:
- 需要编译所有声明了native函数的Java类,每个所生成的class文件都得用javah生成一个头文件。
-
javah生成的JNI层函数名特别长,书写起来很不方便
。 -
初次
调用native函数时要根据函数名字搜索对应的JNI层函数来
建立关联关系
,这样会影响运行效率
。
有什么办法可以克服上面三个弊端吗?根据上面的介绍可知,Java native函数是通过函数指针来和JNI层函数建立关联关系的。如果
直接让native函数知道JNI层对应函数的函数指针
,不就万事大吉了吗?这就是下面要介绍的第二种方法:动态注册法。
2.3.2 动态注册
既然Java native函数和JNI函数是一一对应的,那么是不是会有一个结构来保存这种关联关系呢?答案是肯定的。在JNI技术中,用来记录这种一一对应关系的,是一个叫
JNINativeMethod
的结构,其定义如下:
//源码位于:libnativehelper/include_jni/jni.h
typedef struct {
// Java中native函数的名字,不用携带包的路径,例如“native_init”。
const char* name;
// Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
const char* signature;
// JNI层对应函数的函数指针,注意它是void*类型。
void* fnPtr;
} JNINativeMethod;
应该如何使用这个结构体呢?来看MediaPlayer JNI层是如何做的,代码如下所示:
static const JNINativeMethod gMethods[] = {
{
// Java中native函数的函数名
"native_init",
// native_init的签名信息
"()V",
// JNI层对应的函数指针
(void *)android_media_MediaPlayer_native_init
},
};
// This function only registers the native methods
static int register_android_media_MediaPlayer(JNIEnv *env)
{
// 调用AndroidRuntime的registerNativeMethods函数,
// 第二个参数表明是Java中的哪个类
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaPlayer", 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使用而提供的一个帮助函数,其代码如下所示:
其实动态注册的工作,只用两个函数就能完成。总结如下:
/*
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中了。