美文网首页
深入了解Android读书笔记——深入理解JNI

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

作者: 新一代螺丝工 | 来源:发表于2022-09-26 23:33 被阅读0次

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

数据类型转换

基本数据类型转换表

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

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

相关文章

网友评论

      本文标题:深入了解Android读书笔记——深入理解JNI

      本文链接:https://www.haomeiwen.com/subject/vstfartx.html