JNI概述
JNI是 Java Native Interface
的缩写,意为 Java本地接口。
作用:
- Java程序可以调用Native语言(一般指C/C++)写的函数
- Native语言可以调用Java层函数
在Android平台,JNI库采用 lib_模块名_jni.so
的命名方式。
JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数。
下面以Android源码的 MediaScanner
为例
加载动态库
static {
//在linux上是 media_jni.so,在windows上是 media_jni.dll
System.loadLibrary("media_jni");
native_init();
}
一般加载动态库是在静态块中通过System.loadLibrary
来实现的。
注册JNI
MediaScanner
对应的JNI代码在 android_media_MediaScanner.cpp
中。要使Java中的方法与JNI中的方法相对应,你需要注册JNI函数。有两种方式
静态注册(不推荐)
- 编写java文件,编译成 .class
- 使用
java -h
命令生成 .h 文件
实现原理:当Java层调用函数(如 native_init())时,它会从对应的JNI库寻找对应的函数(如 Java_android_media_Scanner_native_init()),如果没有就会报错。如果找到则会为这两个函数建立一个关联关系,其实就是保存JNI层函数的函数指针。以后调用这个函数时,直接使用就可以了。
不足:
- 需要编译所有声明了
native
的函数的java类,并且需要每个为它们生成一个 .h 文件 - javah 生成的JNI函数名特别长
- 初次调用会建立关系,影响运行效率
动态注册(推荐)
动态注册的结构
typedef struct {
const char* name;//java的native方法的函数名
const char* signature;//java的native方法的签名信息
void* fnPtr;//JNI层对应函数指针
} JNINativeMethod;
示例代码如下
static const JNINativeMethod gMethods[] = {
...
{
"native_init",
"()V",
(void *)android_media_MediaScanner_native_init
},
{
"native_setup",
"()V",
(void *)android_media_MediaScanner_native_setup
},
{
"native_finalize",
"()V",
(void *)android_media_MediaScanner_native_finalize
},
};
将结构注册的方法是
//这里的className是java类的全路径名
jclass clazz = (*env) ->FindClass(env, className);
//注册关联关系,Android中提供了JNIHelp,其内部有jniRegisterNativeMethods方法封装了这些步骤
(*env)->RegisterNatives(env, clazz, gMethods, numMethods);
当Java层通过System.loadLibrary
加载完动态库后,会查找该库的JNI_OnLoad
函数。如果有的话,就会调用它。因此我们需要在代码中实现这个函数,并在函数中调用注册结构的方法。
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
...
//注册关联关系
(*env)->RegisterNatives(env, clazz, gMethods, numMethods);
...
return JNI_VERSION_1_4;//必须返回这个值,否则报错
}
JNI层代码中一般要包含
jni.h
的头文件。Android源码中提供了JNIHelp.h
的帮助头文件,它内部包含了jni.h
。所以代码中直接包含JNIHelp.h
即可
数据类型转换
基本数据类型转换表
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位 |
引用数据类型
Java引用类型 | Native类型 |
---|---|
all objects | jobject |
java.lang.Class | jclass |
java.lang.String | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
java.lang.Throwable | jthrowable |
JNIEnv
上图是 JNIEnv
的内部结构。从图中可知,JNIEnv
实际上就是提供了一些JNI系统函数。通过这些函数可以做到:
调用Java的函数
操作jobject对象等
JNIEnv与JavaVM
在一个线程中有一个JNIEnv
,它是与线程相关的。而 JavaVM
在多线程中也只有一份。通过 JavaVM
的 AttachCurrentThread
函数,就可以得到这个线程的 JavaVM
结构体;另外在退出线程时,需要调用DetachCurrentThread
来释放对应的资源
JNIEnv操作jobject
jfieldID 和 jmethodID
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
为这个函数或者变量的签名信息。
注意:获取java类的成员变量和成员函数是耗时操作,一般把获取到的java类的成员变量和成员函数对象保存到成员变量中,提高程序的运行效率
使用 jfieldID 和 jmethodID
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...)
其中type
是指方法的返回值类型,如 CallVoidMethod
、 CallIntMethod
;如果需要调用静态方法,你需要调用 CallStatic<Type>Method
系列函数。
//获取属性值
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID)
//设置属性值
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value)
jstring
NewString()//从Native字符串得到一个jstring对象
NewStringUTF()//根据Native的一个UTF-8字符串得到一个jstring对象
GetStringChars()//将java string 转换成Unicode字符串
GetStringUTFChars()//将java string转换成UTF-8字符串
ReleaseStringChars()//释放资源,否则会导致JVM内存泄露
ReleaseStringUTFChars()//释放资源,否则会导致JVM内存泄露
JNI类型签名
Java提供一个叫javap
的工具帮助生成函数或者变量的签名信息,用法如下:
javap -s -p xxx
其中 xxx 为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印所有函数和成员的签名信息。默认只会打印public成员和函数的签名信息。
垃圾回收
JNI的三种类型引用
- 本地引用:在JNI层函数中使用的非全局引用对象都是 本地引用 ,它包括函数调用时传入的jobject和在JNI层函数中创建的jobject。本地引用的最大特点是,一旦JNI层函数返回,这些jobject就可能被垃圾回收
- 全局引用:这种对象不主动释放,就永远不会被垃圾回收
- 弱全局引用:是一种特殊的全局引用,它在运行过程中可能被垃圾回收。所以在使用之前,需要调用JNI的
IsSameObject
判断它是否被回收了
static jobject save_thiz = NULL;
save_thiz = jobject;//这样不会增加引用计数,可能被垃圾回收掉,因此需要全局引用
释放引用
DeleteLocalRef()//释放本地变量,当本地变量分配太多内存,而方法执行时间长时,需要处理
DeleteGlobalRef()//释放全局变量
JNI异常处理
如果调用JNI的函数出错了,则会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作了。
JNI层函数可以在代码中捕获和修改这些异常
- ExceptionOccured:用来判断是否发生异常
- ExceptionClear:用来清理当前JNI层发生的异常
- ThrowNew:用来向Java层抛出异常