JNI,是Java Native Interface的缩写,中文为Java本地调用。Android系统底层很多是用Native语言写的,比如Java虚拟机。其实Java一直在使用JNI技术。
Java要调用Native层函数,就必须通过一个位于JNI层的动态库(运行时加载库)才能做到。
Java层的MediaScanner.java
public class MediaScanner implements AutoCloseable {
static {
//加载对应的JNI库,media_jni是JNI库的名字。
//实际加载动态库的时候在Linux上会拓展成libmedia_jni.so,在Windows平台上将拓展为media_jni.dll
System.loadLibrary("media_jni");
native_init();
}
//native为Java的关键字,表示它将由JNI层完成。
private static native final void native_init();
}
复制代码
Java完成下面两个步骤,就可以使用JNI了。
- 加载JNI层动态库: 理论上是在调用native方法前的任何时间、地点都可以加载,通常的做法是在类的
static
语句中通过System.loadLibrary("动态库名字")
加载 - 声明由关键字native修饰的方法
JNI层的android_media_MediaScanner.cpp
//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;
}
复制代码
现在,JNI层都对Java层的方法进行了实现。那么如何保证调用Java方法时,会准确地调用的相对应的JNI函数呢?
JNI函数的注册
“注册”之意就是将Java层的native方法和JNI层对应的实现函数关联起来,有了这种关联,调用Java层的native方法时,就能顺利转到JNI层对应的函数执行了。
注册分为两种:动态注册和静态注册
动态注册
1. 怎么注册
显然,Java的Native方法和JNI层的函数一一对应,在JNI技术中一个叫JNINativeMethod的结构用来记录这种一一对应关系。
typedef struct {
//Java中native方法的名字,不用携带包的路径。例如“native_init”。
constchar* name;
//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
const char* signature;
//JNI层对应函数的函数指针,注意它是void*类型。
void* fnPtr;
} JNINativeMethod;
复制代码
所以,我们可以初始化这些JNINativeMethod
结构体来建立这种一一对应的关联。
--> android_media_MediaScanner.cpp
//定义一个JNINativeMethod数组,其成员就是MediaScanner中所有native函数的一一对应关系。
static JNINativeMethod gMethods[] = {
{
"native_init",
"()V",
(void *)android_media_MediaScanner_native_init //函数指针
},
{
"processFile" //Java中native函数的函数名。
//processFile的签名信息,签名信息的知识,后面再做介绍。
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void*)android_media_MediaScanner_processFile //JNI层对应函数指针。
},
......
};
//注册JNINativeMethod数组
int register_android_media_MediaScanner(JNIEnv*env){
//调用AndroidRuntime的registerNativeMethods函数,第二个参数表明是Java中的哪个类
return AndroidRuntime::registerNativeMethods(env,"android/media/MediaScanner", gMethods, NELEM(gMethods));
}
复制代码
--> AndroidRuntime.cpp
int AndroidRuntime::registerNativeMethods(JNIEnv*env,constchar* className, const JNINativeMethod* gMethods, int numMethods){
//调用jniRegisterNativeMethods函数完成注册
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
复制代码
--> JNIHelp.c
//Android平台中,为了方便JNI使用而提供的一个帮助函数。
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;
}
return 0;
}
复制代码
总结一下就是,JNINativeMethod
记录Java方法和JNI函数的关联关系,最终调用JNIEnv
的函数来初始化JNINativeMethod数组
。
1. clazz= (*env)->FindClass(env, className);
2. (*env)->RegisterNatives(env, clazz, gMethods, numMethods)
复制代码
2. 何时注册,何处注册
上面说明了如何注册 JNINativeMethod数组。那么问题来了,我们什么时候去注册 JNINativeMethod数组呢?答案即将揭晓。 还记得,Java层的方法吗?
public class MediaScanner implements AutoCloseable {
static {
//加载对应的JNI库,media_jni是JNI库的名字。
//实际加载动态库的时候在Linux上会拓展成libmedia_jni.so,在Windows平台上将拓展为media_jni.dll
System.loadLibrary("media_jni");
native_init();
}
//native为Java的关键字,表示它将由JNI层完成。
private static native final void native_init();
}
复制代码
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库(libmedia_jni.so)中一个叫JNI_OnLoad
的函数。如果有这个函数,就调用它。所以,我们就可以在 JNI_OnLoad函数中,进行JNI函数的注册
。所以,要实现动态注册的话,就要实现JNI_OnLoad函数。
libmedia_jni.so的JNI_OnLoad函数是在哪里实现的呢?由于多媒体系统很多地方都使用了JNI,所以码农把它放到android_media_MediaPlayer.cpp中了 -->android_media_MediaPlayer.cpp
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
jintresult = -1;
if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
gotobail;
}
......
//动态注册MediaScanner的JNI函数。
if(register_android_media_MediaScanner(env) < 0) {
goto bail;
}
......
return JNI_VERSION_1_4;//必须返回这个值,否则会报错。
}
复制代码
果不其然,调用了android_media_MediaScanner.cpp
的register_android_media_MediaScanner
进行动态注册。
静态注册:根据函数名来找对应的JNI函数
1. 生成JNI头文件
这种方法需要Java的工具程序javah参与,整体流程如下:
- 先编写Java代码,然后编译生成.class文件。
- 使用Java的工具程序javah。执行javah–o output packagename.classname ,这样它会生成一个叫output.h的
JNI层头文件
。其中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。
这个头文件的名字一般都会使用 packagename_class.h
的样式,例如MediaScanner对应的JNI层头文件就是android_media_MediaScanner.h。
/* DO NOT EDIT THIS FILE - it is machinegenerated */
#include <jni.h> //必须包含这个头文件,否则编译通不过
/* Header for class android_media_MediaScanner*/
#ifndef _Included_android_media_MediaScanner
#define _Included_android_media_MediaScanner
extern "C" {
......
//native_init对应的JNI函数,Java层函数名中如果有一个”_”的话,转换成JNI后就变成了”_l”:native_init -> native_linit
JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_1init(JNIEnv*, jclass);
}
复制代码
2. 何时注册,如何注册
当Java层调用native_init方法
时,它会从对应的JNI头文件查找Java_android_media_MediaScanner_native_linit
。如果没有,就会报错;如果找到,则会为这个native_init和Java_android_media_MediaScanner_native_linit建立一个关联关系(注册)
,其实就是保存JNI层函数的函数指针
。以后再调用native_init函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。
综上,静态注册有以下弊端:
- 需要编译所有声明了native函数的Java类,每个生成的class文件都得用javah生成一个头文件
- JNI层函数的名字必须遵循特定的格式, javah生成的JNI层函数名特别长,书写起来很不方便
初次调用native函数
时要根据方法名字搜索对应的JNI层函数来建立关联关系
,这样会影响运行效率
动态、静态注册对比
动态注册,直接在JNINativeMethod结构体中说明了相应的JNI层的函数指针。这使得Java层调用时通过方法名找到结构体数组中的对应结构体,就可以直接找到对应的JNI层函数指针。
static JNINativeMethod gMethods[] = {
{
"native_init",
"()V",
(void *)android_media_MediaScanner_native_init //函数指针
},
{
"processFile" //Java中native函数的函数名。
//processFile的签名信息,签名信息的知识,后面再做介绍。
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void*)android_media_MediaScanner_processFile //JNI层对应函数指针。
},
......
};
复制代码
而静态注册,Java层首次调用时,要根据方法名字在javah头文件中搜索对应的JNI层函数,并建立关联关系,下次才能直接使用这个函数指针。 一个是一开始就有方法和函数指针的对应关系(结构体);一个是要先通过方法名建立方法和函数指针的对应关系。所以,动态注册效率会好一些。
Java层和JNI层的数据类型转换
Java层
public class MediaScanner implements AutoCloseable {
static {
//加载对应的JNI库,media_jni是JNI库的名字。
//实际加载动态库的时候在Linux上会拓展成libmedia_jni.so,在Windows平台上将拓展为media_jni.dll
System.loadLibrary("media_jni");
native_init();
}
//native为Java的关键字,表示它将由JNI层完成。
private static native final void native_init();
private native void processFile(String path, String mimeType,MediaScannerClient client);
}
复制代码
JNI层
//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;
}
//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);
......
constchar *pathStr = env->GetStringUTFChars(path, NULL);
......
if(mimeType) {
env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
}
}
复制代码
对比上面两端代码可以发现,Java层和JNI层方法/函数 的参数类型不同。这是因为Java层和JNI层存在数据类型转换。
基本数据类型转换关系表
Java类型 | Native类型 | 符号属性(有/无符号) | 字长(位) |
---|---|---|---|
boolean | jboolean | 无 | 8 |
byte | jbyte | 无 | 8 |
char | jchar | 无 | 16 |
short | jshort | 有 | 16 |
int | jint | 有 | 32 |
float | jfloat | 有 | 32 |
long | jlong | 有 | 64 |
double | jdouble | 有 | 64 |
引用数据类型的转换表
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[] | jfloatArray |
byte[] | jbyteArray | double[] | jdoubleArray |
java.lang.Throwable实例 | jthrowable |
从上表可知,All objects -> jobject,所有的引用数据类型在JNI层都是jobject。如果一个方法有多个引用数据的参数,那JNI层把它们都当成是jobject,怎么使用它们呢?
JNIEnv理解
Java层和JNI层方法/函数参数个数不同,都有JNIEnv
JNI类型签名介绍
垃圾回收
其他方面的深入了解请看书。
总结自:《深入了解Android卷I》-邓凡平