JNI概述
- Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++写的函数
- Native层中的函数可以调用Java层的函数,也就是在C/C++程序中调用Java的函数
要想做到以上两点,JNI(Java Native Interface)技术应运而生。
深入分析后可以知道,虽然Java代码具有平台无关性,但运行在具体平台上的Java虚拟机并不能做到这一点,但是JNI技术的出现屏蔽了不同平台的差异,使得上层的Java具有了平台无关的特性。
MediaScanner的分析
android大量使用了JNI技术,书中分析了MediaScanner这个例子。
- Java世界对应的是MediaScanner类,这个类一些函数需要Native层来实现,比如对目录的扫描。
- Native层对应是libmedia.so,他完成了实际的功能。
- JNI层对应的是libmedia_jni.so,这里可以看出JNI层其实也是Native代码写的。其中下划线前的media是Native层库的名字,这里指的就死libmedia库,android对JNI库的名字有规范,一般是:
lib模块名_jni.so
- Java层的MediaScanner将通过libmedia_jni.so来与Native层的libmedia.so交互。
Java层的MediaScanner
framewor/base/media/java/android/media/MediaScanner.java是MediaScanner在Java层源码的位置。
public class MediaScanner
{
static {
/**加载对应的JNI库,media_jni是对应JNI库的名字,实际上加载动态库的时候他会拓展成libmedia_jni.so **/
System.loadLibrary("media_jni");
native_init(); //调用native_init()函数,他是一个native函数
}
......
//扫面目录的函数,里面用到了processDirectory这个native函数
public void scanDirectories(String[] directories, String volumeName) {
......
for (int i = 0; i < directories.length; i++) {
processDirectory(directories[i], mClient);
}
......
private native void processDirectory(String path, MediaScannerClient client);
private static native final void native_init();
......
这里说明了两点内容,在需要native函数的java类中会包括JNI的加载和Native函数的声明,指示它由JNI层去完成。
- JNI加载流程
Java层要调用Native函数,必须通过位于JNI层的动态库才能实现。动态库的必须在运行时加载,所以,我们通常的做法就是在类的static语句中加入System.loadLibrary方法完成对动态库的加载,系统会自动将参数转化成对应环境下真实动态库的名字。linux上是so文件,win下就是dll文件。 - Java的native函数
加载完JNI库后,我们只需要在Java中声明由关键字native修饰的函数即可。是不是很简单,但是JNI层的MediaScanner就没那么轻松了。
JNI层的MediaScanner
JNI层的MediaScanner代码位于
frameworks/base/media/jni/android_media_MediaScanner.cpp
上面再Java层声明的native_init和processFile在这里都有具体的实现,那么问题就来了,Java层声明的函数如何才能对应到JNI层的。
- 注册JNI函数
native_init函数位于android.media这个包中,这样可以得到它的全路径名称android.media.MediaScanner.native_init,之后再将.全部换成_,native_init就找到了JNI层的实现。
上述就是JNI函数注册的问题
-
静态注册
流程如下:
注意:因为packagename是该类完整的包名,所以javah命令需要在包外面执行,否则会找不到该类
静态注册的弊端:1、需要编译所有声明了native函数的java类。2、javah生成的jni函数名会特别长。 3、初次调用native函数时要根据函数名来搜索对应JNI层的函数,并建立关联,这会影响效率。
-
动态注册
既然Java Native函数和JNI函数一一对应,能不能有这种结构可以保存这种关联关系呢,肯定是有的,在JNI技术中有一种JNINativeMethod的结构,它记录下了这些关联关系。其实Android大部分采用的是动态注册的方法
MediaScanner的JNI层中的示例:
static const JNINativeMethod gMethods[] = {
{
"processDirectory",
"(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void )android_media_MediaScanner_processDirectory
},
{
"processFile",
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void )android_media_MediaScanner_processFile
},
.......
{
"native_init",
"()V",
(void )android_media_MediaScanner_native_init
},
......
};
基本结构定义:
typedef struct {
//java类中native函数的名字,不用携带包名,例如“native_init”
const char name;
//java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合
const char signature;
//JNI层对应函数的函数指针,它是void 类型
void* fnPtr;
}JNINativeMethod;定义完毕之后,需要执行注册
int register_android_media_MediaScanner(JNIEnv *env){ return AndroidRuntime::registerNativeMethods(env, kClassMediaScanner, gMethods, NELEM(gMethods)); }
我们发现AndroidRunTime类提供了注册函数,而它又调用了JNIHelp中的jniRegisterNativeMethods方法,最终会走到JNIEnv的RegisterNatives完成注册工作。
那到底是什么时候去执行这个?
在Java层执行System.loadLibrary加载JNI动态库后,紧接着会查找该库中一个JNI_OnLoad的函数,如果有的话,动态注册就在这里完成。然而在MediaScanner对应的android_media_MediaScanner.cpp中并没有发现这个函数。由于多媒体系统中很对地方用到JNI,所以register_android_media_MediaScanner这个注册方法被放在了android_media_MediaPlayer.cpp的JNI_OnLoad方法中,当然还有其它的多媒体相关的注册函数。
数据类型转换
主要解决的问题是:在Java中调用native函数传递的参数是Java数据类型,如何将他们转化成Native的数据类型。
Java分基本数据类型和引用数据类型,先看基本数据类型:
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位 |
这里要注意的是转换成Native类型后对应的数据类型的字长,char在Java中是占两个字节,jchar同样也是,要区别于普通char只占一个字节。
引用类型的转换
Java引用类型 | Native类型 | Java引用类型 | Native类型 | |
---|---|---|---|---|
All object | jobject | char[] | jcharArray | |
java.lang.Class实例 | jclass | short[] | jshortArray | |
java.lang.String实例 | jstring | int[] | jintArray | |
Object[] | jobjectArray | long[] | jlongArray | |
boolean[] | jbooleanArray | flaot[] | jflaotArray | |
byte[] | jbyteArray | double[] | jdoubleArray | |
java.lang.Throwable实例 | jthrowable |
再拿MediaScanner举例,其中的processFile
android_media_MediaScanner_processFile( JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client){
Java层的函数中只传了三个参数,即path,mineType和client,这里它们的类型都转化成了native类型。前两个参数分别是JNIEnv和Java层的MediaScanner对象(jobject)
JNIEnv
JNIEnv其实是一个与线程相关的代表JNI环境的结构体。
JNIEnv提供了JNI相关的系统函数,这些函数可以做到:调用Java函数和操作jobject对象等。
- 通过JNIEnv操作jobject
在JNI规则中,用jfiledID和jMethodID来表示java类的成员变量和成员函数。比如MyMediaScannerClient中。
代码中将scanFile和handleStringTag函数的jmethodID保存为MyMediaScannerClient的成员变量,这是为了解决每次操作object都会去查询jmethodID或jfieldID而带来的运行效率降低。
使用可以看virtual status_t scanFile中的mEnv->CallVoidMethod
完成JNI层调用Java对象的函数。 - jstring
JNIEnv提供有关jstring的函数 - JNI签名介绍
Java函数支持函数重载,也就是说,可以定义同名但是不同参数的函数,但仅仅根据函数名是没法找到具体函数的。为了解决这个问题,JNI技术就将参数类型和返回值类型的组合作为一个函数的签名信息,有了这个签名和函数名,就能很顺利的找到Java中的函数。 - 垃圾回收
JNI的三种类型引用技术:
local reference:本地引用,一旦JNI函数返回时,jobject就可能会被回收
Global reference:全局引用,不主动释放不会被回收
Weak Global reference:特殊的全局引用,可能会被回收,每次使用时需要判断是否回收 -
JNI中的异常处理
JNIEnv提供了三个函数给予帮助
ExceptionOccured函数,用来判断是否发生异常
ExceptionClear函数,用来清理JNI层发生的异常。
ThrownNew函数,用来Java层抛出异常。