第九章 JNI 原理.md
JNI 是 Java Native Interface 的缩写,Java 本地接口。可以简单理解为可以在 Java 中调用其他语言的代码。主要由以下情况需要用到 JNI:
- 需要调用 Java 语言不支持的依赖于操作系统平台特性的一些功能
- 为了整合一些以前的非 Java 语言开发的系统
- 为了节省程序的运行时间,需要使用其他语言来提升运行效率,例如音视频、游戏渲染等
为了更方便的使用 JNI,Android 提供了 NDK 这个工具集合,NDK 开发是基于 JNI 的 。
9.1 系统源码中的 JNI
通过 JNI,Java 的代码可以访问 Native 世界的代码,同样的也可反过来。
这里以 MediaRecorder 框架来举例。
9.2 MediaRecorder 框架中的 JNI
9.2.1 Java Framework 层的 MediaRecord
先来看看 MediaRecorder.java 的源码,这里只截取部分:
public class MediaRecorder implements AudioRouting, AudioRecordingMonitor, AudioRecordingMonitorClient, MicrophoneDirection {
static {
System.loadLibrary("media_jni"); // 1
native_init(); // 2
}
// ……
private static native final void native_init(); //3
// ……
public native void start() throws IllegalStateException;
// ……
}
对于 JavaFramework 层来说只要加载对应的 JNI 库,然后生命 native 方法即可,剩下的工作 JNI 会自动帮我们完成 。
9.2.2 JNI 层的 MediaRecorder
这里 JNI 层的源码在 android_media_recorder.cpp 中,如下所示:
// /frameworks/base/media/jni/android_media_MediaRecorder.cpp
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
return;
}
// ……
fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
"(Ljava/lang/Object;IIILjava/lang/Object;)V");
if (fields.post_event == NULL) {
return;
}
}
static void
android_media_MediaRecorder_start(JNIEnv *env, jobject thiz)
{
ALOGV("start");
sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
if (mr == NULL) {
jniThrowException(env, "java/lang/IllegalStateException", NULL);
return;
}
process_media_recorder_call(env, mr->start(), "java/lang/RuntimeException", "start failed.");
}
当我们调用 native_init 与 start 方法时,最终会通过 JNI 调用到以上方法,接下来是注册过程:
9.2.3 Native 方法注册
分为静态注册和动态注册,其中静态注册多用于 NDK 开发,动态注册多用于 Framework 开发 。
9.2.3.1 静态注册
首先手动写一个 MediaRecorder.java
public class MediaRecorder {
static{
System.loadLibrary("media_jni");
native_init();
}
private static native void native_init();
public native void start() throws IllegalStateException;
}
然后 cd 到项目的 java 路径中,执行以下代码:
这里是新版 java ,与书中指令有出入,同时因为是 win ,路径分隔符为 \
该指令将 c 与 h 两种操作合并,其中第一个参数为 header 文件的路径,这里用 . 表示当前路径
javac -h . com\heyanle\frameworkdemo\MediaRecorder.java
书中原指令:
javac com/example/MediaRecorder.java
javah com.example.MediaRecorder
我们看见在 java 路径生成了 com_heyanle_frameworkdemo_MediaRecorder.h 文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_heyanle_frameworkdemo_MediaRecorder */
#ifndef _Included_com_heyanle_frameworkdemo_MediaRecorder
#define _Included_com_heyanle_frameworkdemo_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_heyanle_frameworkdemo_MediaRecorder
* Method: native_init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_heyanle_frameworkdemo_MediaRecorder_native_1init
(JNIEnv *, jclass); // 1
/*
* Class: com_heyanle_frameworkdemo_MediaRecorder
* Method: start
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_heyanle_frameworkdemo_MediaRecorder_start
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
我们看注释 1 处就是 native_init 方法在 native 中的方法体:
/*
* Class: com_heyanle_frameworkdemo_MediaRecorder
* Method: native_init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_heyanle_frameworkdemo_MediaRecorder_native_1init
(JNIEnv *, jclass); // 1
其中有两个参数,第一个 JNIEnv 指针,该指针中可以在 native 世界中调用 java 世界的代码。而第二个参数 jclass 是 JNI 的书籍类型,对应 Java 的 java.lang.Class 实例,同样的还有 jobject 等数据类型 。
这里 _ 翻译到 jni 会变成 _1 ,因此方法名中为 1init
当我们调用 native 方法时,会自动找到其对应的 h 文件,函数,并建立连接,并且只在第一次调用时建立连接(保存函数指针),之后直接使用。
静态注册有缺点:
- JNI 层函数名过长
- 生命 Native 方法的类需要用 javah 生成头文件
- 初次调用时需要建立关联,影响效率
9.2.3.2 动态注册
JNI 中记录 Native 方法和 JNI 方法的关联关系的结构体是 JNINativeMethod,其在 jni.h 中定义:
typedef struct {
const char* name; // Java 方法名字
const char* signature; // Java 方法签名信息
void* fnPtr; // JNI 中对应方法指针
} JNINativeMethod;
系统的 MediaRecorder 采用的就是动态注册,首先它定义了一个 JNINativeMethod 类型的数组:
// frameworks/base/media/jni/android_media_MediaRecorder.cpp
static const JNINativeMethod gMethods[] = {
// ……
{"start", "()V", (void*)android_media_MediaRecorder_start}, //1
// ……
}
所有的 native 方法一条对应一个 JNINativeMethod对象。
然后再 JNI_OnLoad 函数中调用,该函数会在调用 System.loadLibrary 方法时调用,MediaRecorder 也就是在该函数中注册 JNINativeMethod,
最终回来到 AndroidRuntime.cpp 的 registerNativeMethods 函数:
// frameworks/base/core/jni/AndroidRuntime.cpp
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods){
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
最终来到了 JNIHelp.cpp 的 jniRegisterNativeMethods 函数,最终会调用 JNIEnv 的 RegisterNatives 函数来完成注册,JNIEnv 在 JNI 中非常重要。
9.3 数据类型转换
9.3.1 基本数据类型
9.3.2 引用数据类型
9.4 方法签名
方法签名是 JNI 为了支持 Java 的方法重载而规定的一个格式,一个方法签名对应着一个方法,包括参数和返回值信息。例如一个 Java 方法:
private native final void native_setup(Object mediarecorder_this, String clientName, String opPackageName) throws IllegalStateException;
该方法在 JNI 中的签名为:
(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String)V
这里 V 代表 void,也就是没有返回值,具体类型可以参考之前的表格
9.5 解析 JNIEnv
JniEnv 的主要作用:
- 调用 Java 的方法
- 操作 Java (实例中的变量和对象)
定义:
// libnativehelper/include/nativehelper/jni.h
#if defined(__cplusplus)
// CPP
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
# else
// C
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
JavaVM 是 JVM 在 JNI 中的抽象,一个 JVM 进程只会有一个 实例,该进程中所有线程都可以使用,调用 JavaVM::AttachCurrentThread
函数可以获取该 JVM 的 JNIEnv 对象 。使用结束后需要调用 JavaVM::DetachCurrentThread
函数来释放资源。
_JNIEnv 是一个结构体,其内部又包含了 JNINativeInterface ,只是为了一些 CPP 的特性而进行的包装。_JNIEnv 中定义了很多函数,这里列举三个
- FindClass 寻找 Java 中指定名称的类
- GetMethodID 得到 Java 中指定的方法
- GetFieldID 得到 Java 中指定的成员变量
9.5.1 jfieldID 和 jmethodID(PASS)
9.5.2 使用 jfieldID 和 jmethodID (PASS)
9.6 引用类型
JNI 有引用类型,分别是本地引用,全局引用和弱全局引用
9.6.1 本地引用 (Local References)
本地引用的特点:
- 当 Native 函数返回时,该引用会被释放
- 只在创建它的线程中有效,不能跨线程
- 拒不引用是 JVM 负责的引用类型,受 JVM 管理
JNIEnv 提供的函数返回的引用基本都是 本地引用,可以调用 JNIEnv::DeleteLocalRef
函数立即删除本地引用
9.6.2 全局引用 (Global References)
- 在 native 函数返回时不会被自动释放,并且不会被 GC
- 可以跨线程
- 不受 JVM 管理
调用 JNIEnv::NewGlobalRef
函数创建全局引用,调用 JNIEnv::DeleteGlobalRef
来释放全局引用 。
两者都传入对应的 jclass 类型参数。
9.6.3 弱全局引用 (Weak Global References)
与 全局引用类型,区别只在于会被 GC 回收,回收后指向 NULL。
调用 JNIEnv::NewWeakGlobalRef
函数创建弱全局引用,调用 JNIENV::IsSameObject
与 JNIEnv::DeleteWeakGlobalRef
函数来判断是否被回收与释放引用