第九章 JNI 原理.md

何言 2021年10月22日 39次浏览

JNI 是 Java Native Interface 的缩写,Java 本地接口。可以简单理解为可以在 Java 中调用其他语言的代码。主要由以下情况需要用到 JNI:

  • 需要调用 Java 语言不支持的依赖于操作系统平台特性的一些功能
  • 为了整合一些以前的非 Java 语言开发的系统
  • 为了节省程序的运行时间,需要使用其他语言来提升运行效率,例如音视频、游戏渲染等

为了更方便的使用 JNI,Android 提供了 NDK 这个工具集合,NDK 开发是基于 JNI 的 。

9.1 系统源码中的 JNI

image20211003144456625.png

通过 JNI,Java 的代码可以访问 Native 世界的代码,同样的也可反过来。

这里以 MediaRecorder 框架来举例。

9.2 MediaRecorder 框架中的 JNI

image20211003144722519.png

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 基本数据类型

image20211009160036213.png

9.3.2 引用数据类型

image20211009160227255.png

image20211009160236848.png

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::IsSameObjectJNIEnv::DeleteWeakGlobalRef 函数来判断是否被回收与释放引用