深入了解Android读书笔记——深入理解JNI

417 阅读6分钟

JNI概述

JNI是 Java Native Interface 的缩写,意为 Java本地接口。

作用:

  1. Java程序可以调用Native语言(一般指C/C++)写的函数
  2. 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函数。有两种方式

静态注册(不推荐)

  1. 编写java文件,编译成 .class
  2. 使用 java -h 命令生成 .h 文件

实现原理:当Java层调用函数(如 native_init())时,它会从对应的JNI库寻找对应的函数(如 Java_android_media_Scanner_native_init()),如果没有就会报错。如果找到则会为这两个函数建立一个关联关系,其实就是保存JNI层函数的函数指针。以后调用这个函数时,直接使用就可以了。

不足:

  1. 需要编译所有声明了 native 的函数的java类,并且需要每个为它们生成一个 .h 文件
  2. javah 生成的JNI函数名特别长
  3. 初次调用会建立关系,影响运行效率

动态注册(推荐)

动态注册的结构


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即可

数据类型转换

基本数据类型转换表

JavaNative类型符号属性字长
booleanjboolean无符号8位
bytejbyte无符号8位
charjchar无符号16位
shortjshort有符号16位
intjint有符号32位
longjlong有符号64位
floatjfloat有符号32位
doublejdouble有符号64位

引用数据类型

Java引用类型Native类型
all objectsjobject
java.lang.Classjclass
java.lang.Stringjstring
Object[]jobjectArray
boolean[]jbooleanArray
byte[]jbyteArray
char[]jcharArray
short[]jshortArray
int[]jintArray
long[]jlongArray
float[]jfloatArray
double[]jdoubleArray
java.lang.Throwablejthrowable

JNIEnv

image.png

上图是 JNIEnv 的内部结构。从图中可知,JNIEnv 实际上就是提供了一些JNI系统函数。通过这些函数可以做到:

  1. 调用Java的函数

  2. 操作jobject对象等

JNIEnv与JavaVM

在一个线程中有一个JNIEnv,它是与线程相关的。而 JavaVM 在多线程中也只有一份。通过 JavaVMAttachCurrentThread 函数,就可以得到这个线程的 JavaVM 结构体;另外在退出线程时,需要调用DetachCurrentThread 来释放对应的资源

JNIEnv操作jobject

jfieldID 和 jmethodID

jfieldIDjmethodID 分别表示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 是指方法的返回值类型,如 CallVoidMethodCallIntMethod ;如果需要调用静态方法,你需要调用 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层抛出异常

JNI完整文档看 Java Native Interface Specification Contents