JNI是Java Native Interface,翻译为Java本地接口,是Java与其他语言通信的桥梁,当查询一些用Java技术无法处理的任务时,开发人员就可以使用JNI技术来完成。一般来说主要有以下情况需要用到JNI技术。
- 需要调用Java语言不支持的依赖于操作系统特性的一些功能。
- 为了整合一些以前的非Java语言开发的系统。
- 为了节省程序的运行时间,必须采用其他语言来提升运行效率。
JNI不只是应用于Android开发,它还有非常广泛的应用场景。而在Android的应用场景一般有:音频开发,热修复,插件化,逆向开发,系统源码调用等等。为了更加方便的使用JNI技术,Android还提供了NDK这个工具集合。
系统源码中的JNI:
Android系统语言来划分的话由两个世界组成,分别是java世界和Native世界。这样划分的原因除了性能之外,还有在Java诞生之前,很多程序和库都是由Native语言写的。那么java世界的代码要怎么使用Notive世界的代码呢?答案是使用JNI。这里举例MediaRecorder框架。
1.MediaRecorder框架中的JNI:
在这里重点介绍MediaRecorder框架中JNI,Java Framework层对应的是MediaRecorder.java,JNI层对应的是libmedia_jni.so,它是一个JNI的动态库,Native层对应的是libmedia.so,这个动态库完成了实际上的调用功能。
1.1:Java Framework层的MediaRecorder:
与之相关的有:调用System.loadLibrary("media_jni")用来加载名称为media_jni的动态库,也就是libmedia_jni.so。接着调用native_init方法,其内部会调用Native方法,用来完成JNI的注册。然后再使用native修饰方法,说明它是一个native方法,表示由JNI实现。因为对于JavaFramework层来说只需要加载对应的JNI库,接着声明native方法就可以了,剩下的工作由JNI层来完成。
1.2:JNI层的MediaRecorder:
MediaRecorder的JNI层是由android_media_recorder.cpp实现的android_media_MediaRecorder_native_init方法是native_init方法在JNI层的实现,android_media_MediaRecorder_start方法则是start方法在JNI层的实现。那么native_init方法是怎么找到android_media_MediaRecorder_native_init方法的呢,这需要了解JNI注册。
2.Native方法注册:
Native方法注册分为静态注册和动态注册,其中静态注册多用于NDK开发,而动态注册多用于Framework开发。
2.1:静态注册:
根据函数名找到对应的JNI函数;Java层调用某个函数时,会从对应的JNI中寻找该函数,如果没有就会报错,如果存在就会建立一个关联关系(其实就是保存JNI的函数指针),以后再调用时会直接使用这个函数,这部分的操作由虚拟机完成。
静态注册的建立:首先在Java代码中声明native函数,然后通过javac或者javah相关命令来生成native函数的对应的头文件,然后在c/c++文件中引用这些头文件,最后在JNI代码中实现这些函数的具体业务逻辑即可。
JNI的命名规范:返回值 + Java前缀+全路径类名+方法名+参数1JNIEnv+参数2jobject+其他参数
2.1.1:静态注册的缺点:
- JNI的函数名称过长。
- 声明Native方法的类需要生成头文件。
- 初次调用Native方法时需要建立关联,影响效率。
2.2:动态注册:
JNI中有一种结构体用来记录Java的Native方法和JNI方法的映射关系,它就是JNINativeMethod,在jni.h中定义。JNI允许我们提供一个函数映射表,注册给Java虚拟机,这样JVM就可以用函数映射表来调用相应的函数。这样就可以不必通过函数名来查找需要调用的函数了。
动态注册的建立:在使用System.LoadLibrary函数会调用JNI_OnLoad函数,并且在这个函数里面去动态的注册native方法。在JNI_OnLoad函数中又主要调用了jniRegisterNativeMethods函数,该函数内部调用了RegisterNatives函数
将注册函数的Java类,以及注册函数的数组(JNINativeMethod),以及个数注册在一起,这样就实现了绑定。
3.数据类型的转换:
Java的数据类型到了JNI层就需要转换为JNI层的数据类型。Java的数据类型分为引用数据类型和脚本数据类型,JNI层也对这两种数据类型进行了区分。
3.1:基本数据类型的转换:
Java | Native | Signature |
---|---|---|
byte | jbyte | B |
char | jchar | C |
double | jdouble | D |
float | jfloat | F |
int | jint | I |
short | jshort | S |
long | jlong | J |
boolean | jboolean | Z |
void | void | V |
3.2:引用数据类型的转换:
Java | Native | Signature |
---|---|---|
所有对象 | jobject | L+classname+; |
Class | jclass | Ljava/lang/Class; |
String | jstring | Ljava/lang/String; |
Throwable | jthrowable | Ljava/lang/Throwable; |
Object[] | jobjectArray | [L+classname+; |
byte[] | jbyteArray | [B |
char[] | jcharArray | [C |
double[] | jdoubleArray | [D |
float[] | jfloatArray | [F |
int[] | jintArray | [I |
short[] | jshortArray | [S |
long[] | jlongArray | [J |
boolean[] | jbooleanArray | [Z |
4.方法签名:
因为Java是支持函数重载的,也就是说,可以定义相同方法名,但是不同参数的方法,然后Java根据其不同的参数,找到其对应的实现的方法。JNI要怎么支持这种,答案是---签名。即将参数类型和返回值类型的组合。如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以顺序的找到对应的Java层中的函数了。
- 方法签名的格式:(参数的签名格式,两个参数之间使用;分隔)返回值的签名格式。
- 查看类中的方法签名使用javap命令。
5.解析JNIEnv:
JNIEnv是Native世界中Java环境的代表,通过JNIEnv*指针就可以在Native世界中访问Java世界的代码进行操作,它只在创建它的线程中有效,不能跨线程传递,因此不同线程的JNIEnv是彼此独立的。JNIEnv的主要作用有:
- 调用java的方法。
- 操作java中的变量或者对象等等。
JavaVM它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此该进程的所有的线程都可以使用这个JavaVM。通过JavaVM的AttachCurrentThread函数就可以获取这个线程的JNIEnv,还要记得在使用AttachCurrentThread函数的线程退出之前,调用DetachCurrentThread函数来释放资源。
JNIEnv是一个结构体,其内部又包含了JNIINativeInterface。在JNIEnv中定义了很多的函数,例如:FindClass函数用来找到Java中指定名称的类;GetObjectClass函数通过对象的实例获取class。GetMethodID函数用来获取Java中的方法;GetFieldID函数用来获取Java中的成员变量。JNIEnv的类型都和JNIINativeInterface结构有关联的,在JNIINativeInterface结构中定义了很多和JNIEnv结构体对应的函数指针,通过这些函数指针的定义,就可以确定到虚拟机的JNI函数表中,从而实现了JNI层在虚拟机中的函数调用,这样的话JNI层就可以调用Java世界的方法了。
5.1:jfieldID和jmethodID:
在JNIEnv结构体中定义了很多的函数,这些函数都会有不同的返回值。例如:GetMethodID函数它返回值为一个jmethodID,GetFieldID函数它返回值为一个jfieldID。除此之外还有很多的返回值。JNI层通过找到函数找到Java层的class对象之后,再获取到class的一些成员变量或者方法,赋值给jfieldID和jmethodID类型的变量,具体是jfieldID代表Java的成员变量和jmethodID代表Java的方法。这样做的原因有:
- 如果每次调用相关的方法时都要查询方法和变量,显得效率低。
- 这些方法和成员变量都是本地引用,为了高效使用。
6.引用类型:
与Java一样,JNI也有引用类型,分别是本地引用,全局引用和弱全局引用。
6.1:本地引用:
JNIEnv函数所返回的引用基本上都是本地引用,本地引用是JNI中最常见的引用类型,它的特点有:
- 当Native函数返回时,这个本地引用就会被自动释放。
- 只有在创建它的线程中才有效,不能跨线程使用。
- 局部引用是JVM负责的引用类型,受JVM管理。
- 我们可以调用DeleteLocalRef函数手动删除本地引用。
6.2:全局引用:
全局引用几乎是与本地引用相反的,它的特点有:
- 在Native函数返回时不会被自动的释放,因此全局引用需要手动的进行释放,并且还不会被GC回收。
- 全局引用是可以跨线程使用的。
- 全局引用是不会受到JVM管理。
- JNIEnv的NewGlobalRef函数用于创建全局引用;JNIEnv的DeleteGlobalRef函数用于释放全局引用。
6.3:弱全局引用:
弱全局引用是一种特殊的全局引用,它和全局引用的特定类似,不同的是弱全局引用是可以被GC回收的,弱全局引用被GC回收之后就会指向NULL。JNIEnv的NewWeakGlobalRef函数用于创建弱全局引用;JNIEnv的DeleteWeakGlobalRef函数用于释放弱全局引用。在使用它之前还要JNIEnv的IsSameObject函数来判断是不是已经被GC回收了。