一、jni是什么
java代码要使用native的代码,需要一个桥梁将他们连接起来,这个桥梁就是jni。
二、JNI的举例
1、新建一个Android项目,在根目录下创建 jni文件夹,用于存放 C源码。
2、在java代码中,创建一个本地方法 getStringFromC 本地方法没有方法体。
private native String getStringFromC();
3、在jni中创建一个C文件,定义一个函数实现本地方法,函数名必须用使用 本地方法的全类名,点改为下划线。
#include <stdio.h>
#include <stdlib.h>
#include <jni.h>
//方法名必须为本地方法的全类名点改为下划线,传入的两个参数必须这样写,
//第一个参数为java虚拟机的内存地址的二级指针,用于本地方法与java虚拟机在内存中交互
//第二个参数为一个java对象,即是哪个对象调用了这个 c方法
jstring Java_com_mwp_jnihelloworld_MainActivity_getStringFromC(JNIEnv* env,jobject obj){
//定义一个C语言字符串
char* cstr = "hello form c";
//返回值是java字符串,所以要将C语言的字符串转换成java的字符串
//在jni.h 中定义了字符串转换函数的函数指针
//jstring (*NewStringUTF)(JNIEnv*, const char*);
jstring jstr2 = (*env) -> NewStringUTF(env, cstr);
return jstr2;
}
4、在jni中创建 Android.mk文件,用于配置 本地方法
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
#编译生成的文件的类库叫什么名字
LOCAL_MODULE := hello
#要编译的c文件
LOCAL_SRC_FILES := Hello.c
include $(BUILD_SHARED_LIBRARY)
5、在jni目录下执行 ndk-build.cmd指令,编译c文件
6、在java代码中加载编译后生成的so类库,调用本地方法,将项目部署到虚拟机上之后就会发现toast弹出的C代码定义的字符串
7、jni打包的C语言类库默认仅支持 arm架构,需要在jni目录下创建 Android.mk 文件添加如下代码可以支持x86架构
APP_ABI := armeabi armeabi-v7a x86
三、native方法注册
native方法注册包括静态注册和动态注册,静态注册多用于NDK开发,上述的"二、JNI的举例"就是用的静态注册,而动态注册多用于Fremework开发,下面我们分别了解一下这两种注册方式
1、静态注册
在Android Studio中新建一个java library,命名为media,写一个简单的MediaRecorder.java(仿照系统的MediaRecorder.java)
package com.example;
public class MediaRecorder {
static {
System.loadLibrary("media_jni");
native_init();
}
private static native final void native_init();
public native void start() throws IllegalStateException;
}
然后进入项目的media/src/main/java/com/example目录执行如下命令:
javac -h ./ MediaRecorder.java
说明: javah从java10开始被移除掉,取而代之的是javac -h命令
然后在当前目录会生成com_example_MediaRecorder.h文件,此文件的内容为
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_MediaRecorder */
#ifndef _Included_com_example_MediaRecorder
#define _Included_com_example_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_MediaRecorder
* Method: native_init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_native_1init
(JNIEnv *, jclass);
/*
* Class: com_example_MediaRecorder
* Method: start
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_start
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
我们可以看到native_init的jni方法对应的是Java_com_example_MediaRecorder_native_1init,以"Java"开头说明是在java平台调用JNI方法的,后面的com_example_MediaRecorder_native_1init指的是包名+类名+方法名,中间用" _ "代替。另外我们注意到,原本"native_init"方法名中就有" _ ",为了消除歧义,这里的" _ "就会替换为" _1 "。
JNIEnv是native世界中java环境的代表,通过JNIEnv*指针就可以在native世界中访问java世界的代码进行操作,它只能在创建它的线程中有效,不能跨进程传递。
jclass是jni的数据类型,对应的是java的java.lang.Class实例。
jobject也是jni的数据类型,对应java的object。
当我们在java中调用natice_init方法时,就会从jni中寻找Java_com_example_MediaRecorder_native_1init,如果找到了该方法名的jni函数,就会建立联系,建立联系的方法就是保存jni的函数指针。通过方法名来建立联系的方式就是静态注册。
2、动态注册
从静态注册我们可以看出,只要java层和native层能够进行关联就能完成注册,静态注册采用的是方法名对应,然后保存jni函数指针的方法。动态注册其实就是不靠方法名来进行关联,而是换一种方式来记录"java的native方法"和"jni中的函数指针"的关系的注册方式。
在jni中有一个专门的结构体来描述这种对应关系:
Android系统的MediaRecorder采用的就是动态注册:
通过这个数组,我们就能获取到java层的native函数和jni层的函数指针的一一对应关系,但知道了对应关系还不够,这里只是数组的声明,我们还得使用他,即调用注册函数,才能真正建立联系。
注册函数一般流程:jni的register函数--->AndroidRuntime.registerNativeMethods()---->JNIEnv.RegisterNatives()
四、数据类型转换
通过natice方法的注册,我们已经找到了java层的函数和jni的函数指针的关联关系,但是他们之间相互调用,数据类型也要相匹配才行。换句话说,java层是一个int型变量,native层也需要有native所能理解的int才行,这就需要数据类型转换。
数据类型转换我们又分为基本数据类型转换和引用数据类型转换。
1、基本数据类型转换
2、引用数据类型转换
五、方法签名
我们再来回顾下动态注册的JNINativeMethod数组:
因为java中有函数的重载,所以只通过函数名,我们无法定位到java所指向的函数,于是我们通过方法签名来表示java层的函数的参数和返回值,从而达到定位。如上图数组的元素的第二个参数"()V"和"(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"就是方法签名。
jni的方法签名的格式为:
(参数签名格式...)返回值签名格式。
每次去手写签名格式显然是很心累的一件事,好在java提供了javap命令来自动生成签名。
我们接着在刚才的shell窗口输入
javap -s -p ./MediaRecorder.class
六、解析JNIEnv
JNIEnv是native世界中java环境的代表,它只在创建它的线程中有效,不能跨线程传递,不同线程的JNIEnv彼此独立。
1、JNIEnv的定义:
这里对c和c++做了区分,实际上我们深入源码可以发现,c++中的_JNIEnv结构体实际上又包含了JNINativeInterface,所以无论是c++还是c,最终都是靠JNINativeInterface来实现的,JNINativeInterface的定义如下:
在JNINativeInterface中有很多JNIEnv结构体对应的函数指针,通过这些函数指针的定义,就能定位到虚拟机中的jni函数表,从而实现了jni层在虚拟机中的函数调用,这样jni层就可以调用java世界的方法了。
这里我们又提到了虚拟机,我们再回调"JNIEnv定义"那幅图中,可以看到,除了JNINativeInterface,无论是c++还是c,都还有一个JavaVM变量,它就是虚拟机在jni层的代表,一个虚拟机进程只有一个JavaVM,因此所有线程通能使用这个JavaVM。通过JavaVM的attachCurrentThread函数可以获取到这个线程的JNIEnv,这样就能在不同的线程中调用java方法了。顺便提一下,在线程结束的时候,还需要DetachCurrentThread函数来释放资源。
2、jfieldID和jmethodID
JNIEnv最终都是调用的JNINativeInterface,在JNINativeInterface里面,我们可以看到,函数指针返回的类型就有jfieldID和jmethodID,当然还有其他的,不过都大同小异。jfieldID和jmethodID分别来代表java类中的成员变量和方法。
我们来看一下系统层的MediaRecorder框架的jni层是如何使用GetMethodID
和GetFieldID这两个方法的,如图所示:
可以看到,首先会通过FindClass找到java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz,clazz就是java层的mediaRecorder在jni层的代表,在注释2和注释3分别找到jfava层的MediaRecorder中名为mNativeContext和surface并缓存到fields中,注释4获取到java层的MediaRecorder中名为postEventFromNative方法,并缓存到fields中。这里为什么要进行缓存呢,第一个是因为效率问题,不用每次都查询,第二个是因为这些成员变量和方法都是本地引用,在android_media_MediaRecorder_native_init函数返回时这些本地引用会自动释放。本地引用后续会提到。
3、使用jfieldID和jmethodID
上述只是将jfieldID和jmethodID保存了起来,还没有使用到,那要怎么才能使用呢?如下图所示:
在注释1出调用了JNIEnv的CallStaticVoidMethod函数,其中就传入了缓存的fields.post_event,它其实是保存了java层MediaRecorder的静态方法postEventFromNative,也就是说JNIEnv的CallStaticVoidMethod函数可以访问java的静态方法,同理如果想要访问java的方法则可以使用JNIEnv的CallVoidMethod函数,如果想要想要访问java的属性,可以使用GetObjectField函数。
七、jni的引用类型
jni的引用类型分别是本地引用、全局引用、弱全局引用
1、本地引用
本地引用有以下三个特点:
- 当native函数返回时,这个本地引用就会被自动释放
- 只在创建它的线程中有效,不能够跨线程使用
-
局部引用是JVM负责的引用类型,受JVM管理
注释1处的FindClass会返回clazz,这个clazz就是本地引用,它会在android_media_MediaRecorder_native_init函数调用返回后被自动释放。
2、全局引用
全局引用有以下三个特点:
- 在native函数返回时不会被自动释放,因此全局引用需要手动来进行释放,并且不会被GC回收
- 全局引用是可以跨线程是用的
-
全局引用不受到JVM管理
JNIEnv的NewGlobalRef函数用来创建全局引用,JNIEnv的DeleteGlobalRef函数用来释放全局引用
3、弱全局引用
弱全局引用和全局引用特点差不多,区别是弱全局引用可以被GC回收,回收之后指向NULL,JNIEnv的NewWeakGlobalRef用来创建弱全局引用,JNIEnv的DeleteWeakGlobalRef用来释放弱全局引用。由于弱全局引用可能为NULL,因此使用前要想判断是否为空,使用JNIEnv的sSameObject进行判断