深入理解“JNI”
深入理解“JNI”
一、JNI概述
JNI是Java Native Interface的缩写,中文译为“Java本地调用”。
JNI是一种技术,通过它可以做到:
- Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。
- Native程序中的函数可以调用Java层的函数,也就是说在C/C++程序中可以调用Java的函数。
JNI技术的推出有以下几个方面的意义:
- Java的虚拟机是用Native语言写的,而虚拟机又运行在具体的平台中,所以虚拟机本身是无法做到平台无关的。但是,有了JNI技术就可以对Java层屏蔽不同操作系统平台之间的差异。这样就可以实现Java本身的平台无关特性。
- 在Java语言之前,很多程序都是用Native语言写的,它们遍布在软件世界的各个角落。Java出世后,受到了开发者的热捧,并迅速发展。在Java中通过JNI技术直接使用已经有Native模块更为方便,不用“重复制造轮子”。另外,在一些追求效率和速度的场合还是需要Native语言参与的。
在Android平台中,JNI就是一座将Native和Java连接起来的桥梁,将两种语言紧密地联系在了一起。
JNI示意图.png
二、MediaScanner
MediaScanner是Android平台中多媒体系统的重要组成部分,其功能是扫描媒体文件,得到例如歌曲时长、歌曲作者等媒体信息,并将它们存入到媒体数据库中,供其他应用程序使用。
MediaScanner和其相关的JNI如下图所示:
MediaScannerJNI.png从上图可知:
- Java对应的是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
交互。
三、Java层的MediaScanner分析
MediaScanner的源码中与JNI有关部分的代码如下所示:
public class MediaScanner
{
static{//static语句
/*
1、加载对应的JNI库,media_jni是JNI库的名字。
在实际加载动态库的时候会将其扩展成libmedia_jni.so,
在Windows平台上则会扩展为media_jni.dll
*/
System.loadLibrary("media_jni");
native_init();//调用native_init函数
}
//非native函数
public void scanDirectories(String [] directories,String volumeName){
......
}
//2、声明一个native函数。native为Java的关键字,表示它将由JNI层完成。
private static native final void native_init();
private native void processFile(String path,String mimeType,MediaScannerClient client);
}
代码中有两个要点:
- 加载JNI库;
- Java的native函数。
1.加载JNI库
如果Java要调用native函数,就必须通过一个位于JNI层的动态库
(运行时加载的库)来实现。
在什么时候以及什么地方加载动态库呢?
原则上:在调用native函数前,任何时候、任何地方加载都是可以的。通常的做法是在类的static语句中加载,调用System.loadLibrary方法就可以了
。System.loadLibrary函数的参数是动态库的名字,即media_jni。
2.Java的native
从代码中可知,native_init和processFile函数前都有Java的关键字native,它表示这两个函数将由JNI层来实现。
使用JNI的步骤:
- 加载对应的JNI库。
- 声明由关键字native修饰的函数。
四、JNI层MediaScanner的分析
MediaScanner的JNI层代码在android_media_MediaScanner.cpp中。
//1.这个函数是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;
}
//2.这个函数是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);
......
const char *pathStr = env->GetStringUTFChars(path,NULL);
......
if(mimeType){
env->ReleaseStringUTFChars(mimeType,mimeTypeStr);
}
}
1.注册JNI函数
native_init函数对应的JNI函数是android_media_MediaScanner_native_init。native_init函数位于android.media包中,其全路径名是android.media.MediaScanner.native_init
。而JNI函数的名字是android_media_MediaScanner_native_init
。因为在Native语言中,符号"."有特殊的意义,所以JNI层需要把Java函数名称中的"."换成"_"。
JNI函数的注册,就是将Java层的native函数和JNI层对应的实现函数关联起来,有了这种关联,调用Java层的native函数的时候就可以顺利转到JNI层对应的函数执行了。
JNI函数的注册方式有如下两种:
1.静态方法:根据函数名来找对应的JNI函数,需要Java的工具程序javah参与。整体流程如下所示:
- 先编写Java代码,然后编译生成.class文件。
- 使用Java的工具程序javah,例如:javah -o output packagename.classname,这样就会生成一个叫output.h的JNI层头文件。packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。
静态方法中native函数找到对应的JNI函数的过程:
当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层函数来建立关联关系,这样会影响运行效率。
2.动态注册:在JNI技术中,用JNINativeMethod来记录Java native函数和JNI函数的一一对应关系。
动态注册只用完成下面两个函数即可:
/*
env指向一个JNIEnv结构体。classname为对应的Java类名,由于JNINativeMethod中使用的函数名并非全路径名,所以要指明是哪个类。
*/
jclass clazz = (*env)->FindClass(env,className);
//调用JNIEnv的RegisterNatives函数,注册关联关系。
(*env)->RegisterNative(env,clazz,gMethods,numMethods);
在什么时候和什么地方调用这些动态注册的函数?
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数。如果有,就调用它,而动态注册的工作就是在这里完成的。
2.数据类型转换
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这两种数据类型的。
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位 |
2、引用数据类型的转换
引用数据类型的转换关系如下所示:
Java引用类型 | Native类型 |
---|---|
All objects | jobject |
java.lang.Class实例 | jclass |
java.lang.String实例 | jstring |
Object[] | jobjectArray |
boolean[] | jobjectArray |
byte[] | jbyteArray |
java.lang.Throwable实例 | jthrowable |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | floatArray |
double[] | jdoubleArray |
3.JNIEnv介绍
JNIEnv是一个与线程相关的代表JNI环境的结构体,其内部结构如下所示:
JNIEnv内部结构简图.png如上图所示,JNIEnv实际上就是提供了一些JNI系统函数。通过它们可以做到:
- 调用Java的函数。
- 操作jobject对象等。
如果线程A中有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不可以在线程B中使用线程A的JNIEnv结构体。
JavaVM和JNIEnv有什么关系?
- 调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了。
- 在后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。
4.JNIEnv操作jobject
一个Java对象是由什么组成的?Java对象是由成员变量和成员函数。
操作jobject的本质就应当是操作对象的成员变量和成员函数。
1、jfieldID和jethodID介绍
在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为这个函数和变量的签名信息。
如果每次操作jobject前都去查询jmethodID或jfieldID,那么将会影响程序运行的效率,所以我们在初始化的时候可以取出这些ID并保存起来以供后续使用。
2、使用jfieldID和jmethodID
通过JNIEnv输出CallVoidMethod,再把jobject、jMethodID和对应的参数传进去,JNI层就能够调用Java对象的函数。
实际上JNIEnv输出了一系列类似CallVoidMethod的函数,形式为NativeType Call<type> Method(JNIEnv *env , jobject obj , jmethodID methodID , ...)。
其中type对应Java函数返回值类型。
如下所示是常用的Get/Set函数:
Get函数 | Set函数 |
---|---|
GetObjectField() | SetObjectField() |
GetBooleanField() | SetBooleanField() |
GetByteField() | SetByteField() |
GetCharField() | SetCharField() |
GetShortField() | SetShortField() |
GetIntField() | SetIntField() |
GetLongField() | SetLongField() |
GetFloatField() | SetFloatField() |
GetDoubleField() | SetDoubleField() |
5.jstring介绍
JNI中的jstring类型表示Java中的String类型。下面是几个关于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对象。
- JNIEnv提供了GetStringChars函数和GetStringUTFChars函数,它们可以将Java String对象转换成本地字符串。
- 如果在代码中调用了上面的几个函数,在做完相关工作后,就需要调用ReleaseStringChars函数或ReleaseStringUTFChars函数来对应地释放资源,否则会导致JVM内存泄漏。
6.JNI类型签名
为什么需要签名信息呢?
因为Java支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名是没法找到具体函数的。为了解决这个问题,JNI技术中就将参数类型和返回值类型的组合作为了一个函数的签名信息,有了签名信息和函数名,就能顺利的找到Java中的函数了。
JNI规范定义的函数签名信息的格式是:
(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示
常见的类型标示如下表:
类型标示 | 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 xxx
参数说明如下:
- xxx为编译后的class文件;
- s表示输出内部数据类型的签名信息;
- p表示打印所有函数和成员的签名信息。
7.垃圾回收
Java中创建的对象最后是由垃圾回收器来回收和释放内存的。
JNI技术提供了下面三种类型的引用:
- Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和在JNI层函数中创建的jobject。Local Reference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。
- Global Reference:全局引用。这种对象如不主动释放,它永远不会被垃圾回收。
- Weak Global Reference:弱全局引用,一种特殊的Global Reference,在运行过程中可能会被垃圾回收。所以在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了。
8.JNI中的异常处理
在JNI层中产生的异常不会中断本地函数的运行,但是一旦产生异常后,就只能做一些资源清理工作了(例如释放全局引用,或者ReleaseStringChars)。如果这时调用除前面所说的函数之外的其他JNIEnv函数,则会导致程序死掉。
JNI层函数可以在代码中截获和修改异常,JNIEnv提供了如下三个函数给予帮助:
- ExceptionOccured函数,用来判断是否发生异常。
- ExceptionClear函数,用来清理当前JNI层中发生的异常。
- ThrowNew函数,用来向Java层抛出异常。