这几天来学习下JNI的知识,参考的文章有:
《Android进阶解密》第九章JNI原理
​​​JAVA基础之理解JNI原理​

JNI是Java Native Interface的缩写,翻译是Java本地接口。它的作用是来桥接Java和其他语言(C/C++),完成交互工作。Android中的语言层可以分成Java层和Native层。为什么会有Native层呢?是因为在Java出现之前,很多的功能和系统都是由Native语言写的,所以Java出现之后就不必重复造轮子,直接调用这些方法就行了,而且Native层更接近汇编语言,所以性能更优。

所以代码中就出现了Java层调用Native层的方法,而这个跨语言的调用过程就是 JNI,下图就是他们的关系:

JNI原理学习_android

当Java语言无法处理一些任务的时候,就可以使用JNI来完成。
下面是几个JNI的应用场景:

  • 需要调用Java语言不支持的依赖于操作系统平台特性的一些功能。
    比如:需要调用当前UNIX系统的某个功能,而Java不支持这个功能的时候,就要用到JNI
  • 在程序对时间敏感或对性能要求特别高时,有必要用到更底层的语言来提高运行效率。
    比如:音视频开发涉及到的音视频编解码需要更快的处理速度,这就需要用到JNI
  • 为了整合一些以前的非Java语言开发的系统。
    比如:需要用到早期实现的C/C++语言开发的一些功能或者系统,将这些功能整合到当前的系统或者新的版本中

JNI是完善Java的一个重要功能,它让Java更加全面、封装了各个平台的差异性。
JNI在Android开发里的应用主要有:

  • 音视频开发
  • 热修复
  • 插件化
  • 逆向开发

为了更好的使用JNI,Android提供了NDK这个工具,NDK基于JNI,所以理解了JNI,NDK也会很容易掌握。通过学习JNI,可以把视野从Framework层深入到低层,但是因为在Android应用开发中基本不会用到JNI,所以这边以做理解为主。为之后的热修复插件化等做知识铺垫。

接下来以MediaRecorder框架的JNI为例,来了解JNI的原理。

1.MediaRecorder框架中的JNI

​MediaRecorder​​这个框架用于录音录像。这里不讲解这个框架,而是重点看这个框架中的jni,来看下下图中的关系:

JNI原理学习_java_02


在MediaRecorder框架中,Java Framework层对应的是​​MediaRecorder.java​​,是我们应用开发中直接调用的类。

JNI层中对应的是 ​​libmedia_jni.so​​​,如果一个so库的名称结尾带有 ​​_jni​​,则说明它是一个JNI的动态库。

Native层中对应的是 ​​​​,它是真正实现业务的类。

1.1 Java Framework层的 MediaRecorder

我们先来查看MediaRecorder.java的源码,看看在Java层是如何使用JNI的:

// MediaRecorder.java
public class MediaRecorder
{
static {
// 1
System.loadLibrary("media_jni");
// 2
native_init();
}
....
private static native final voide native_init();
...
public native void start() throws IllegalStateException;
...
}

静态代码是最先加载的。
注释1:加载名为 ​​​media_jni​​​的so库。而真正的这个库名称就是 ​​libmedia_jni​​​,也就是加载这个库
注释2:调用 ​​​native_init()​​,去看它的声明,发现它是一个native方法,也就是说它的实现是在JNI中。

为什么能无中生有一个 ​​native_init()​​​呢,是因为前面加载了一个 ​​libmedia_jni​​​库,这里面肯定封装了这个函数,我们加载进来的动作其实就和 ​​import​​来导包一样,这个成员方法同样的被我导入进来了。

1.2 JNI层的 MediaRecorder

MediaRecorder的JNI层是写在 ​​adnroid_media_MediaRecorder.cpp​​​中的,它实现了 ​​native_init()​​​和 ​​start()​​,在上面的代码中也有看到start方法:

// 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)
{
...
}

​android_media_Media_Recorder_native_init()​​​ 是 JavaFramwork层​​native_init()​​​在JNI层中的实现。
​​​android_media_MediaRecorder_start()​​​ 则是​​start()​​的实现。

这里出现了疑问:
①:我是怎么通过在一开始的 ​​​media_jni​​​这个so库找到 ​​android_media_MediaRecorder.cpp​​​这个JNI实现类?
②:我是怎么知道 ​​​native_init()​​​的JNI实现就是 ​​android_media_MediaRecorder_native_init()​​?它们是怎么对应的?

上述两个问题的答案就是注册,因为有了注册,所以产生了映射关系,让类与类、方法与方法有了联系,我们来看下他们是怎么关联的。

1.3 Native方法注册

Native方法注册分为两种:

  • 静态注册
    多用于NDK开发
  • 动态注册
    多用于Framework层开发

下面我们用实际的例子对两种注册做区分及了解。

1.静态注册

我们在Android Studio一个项目中

右键本项目->new->module->Java Library

LibraryName = media

Java class name = MediaRecorder.java,创建成功后,打开​​MediaRecorder.class​​,并学着MediaRecorder编写:

JNI原理学习_java_03

接着我们对​​MediaRecorder.java​​进行编译和生成JNI方法,我们进入 media/src/main/java中,在 terminal命令行中输入:

javac com/rikkatheworld/media/MediaRecorder.java
javah -jni com.rikkatheworld.media.MediaRecorder

最后会产生了一个 ​​MediaReorder.class​​​和一个 ​​com_rikkatheworld_media_MediaRecorder.h​​​头文件。
上面可能这里可能会有点坑,可以看我 ​​​Android音视频开发入门(5)使用LAME编码一个PCM文件​​第一节怎么去搭建一个JNI项目,这里我就不多赘述。

我们来看下这个被编译出来的头文件的内容:

// com_rikkatheworld_media_MediaRecorder.h
#include <jni.h>

#ifndef _Included_com_rikkatheworld_media_MediaRecorder
#define _Included_com_rikkatheworld_media_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif

// 1
JNIEXPORT void JNICALL Java_com_rikkatheworld_media_MediaRecorder_native_1init
(JNIEnv *, jclass);

JNIEXPORT void JNICALL Java_com_rikkatheworld_media_MediaRecorder_start
(JNIEnv *, jobject);

#ifdef __cplusplus
}

被编译出来的头文件就是我们Java层在JNI层中对应的类。
注释1中的方法:以Java开头,说明这个方法在Java中调用JNI方法的,后面的名称是以 包名+类名+方法名组成格式,还发现 ​​​native_1init()​​​中多了个“1”,这是因为Java声明的​​native_init()​​中有 “_”下划线,转换成到native语言中,就变成了 “_1”。

其中 ​​JNIEnv​​​是Native世界中 Java环境的代表,通过 ​​JNIEnv *​​​这个指针,就可以在Native层中访问Java层的代码并进行操作了,它只在创建它的线程中有效,不能跨线程传递。
​​​jclass​​是JNI型数据,对应Java的 java.lang.Class的实例,是所有对象的父类。

上面这两个数据类型是JNI层常用且关键的数据,后面会对其做介绍。

我们在 Java层中调用 ​​native_init()​​​后,就会从 JNI中寻找 ​​Java_com_rikkatheworld_media_MediaRecorder_native_1init()​​的函数,如果没有,就会报错,如果找到就会为这两个方法建立一个关联,它就会保存一个JNI的函数指针,即一个指针指向了这个函数,等下一次再调用这个方法时,这个指针所指向的函数就能立马被调用。这就是静态注册。

静态注册总结:
根据方法名,将Java方法和JNI函数建立联系,它的缺点是:

  • JNI层的函数名非常长,它是​​Java+包名+类名+方法名​
  • 声明Native方法的类需要用 javah生成头文件
  • 第一次调用时,由于JNI层的指针还未指向对应函数,所以它要一个一个找直到找到,所以每第一次调用新的Native方法时,效率并不会太高

2.动态注册

静态注册是Java的Native通过方法指针来与JNI进行关联的,如果Java的Native方法一开始就知道它在JNI中对应的函数指针,就可以避免上述的缺点,这就是动态注册。
JNI中有一种结构来记录Java的Native方法和JNI方法的关联联系,它就是 ​​​JNINativeMethod​​​,它在​​jni.h​​中的源码是这样的:

// jni.h
typedef struct {
const char* name; //Java方法中的名字
const char* signature; //Java方法的简明信息
void* fnPtr; //JNI中对应的方法指针
} JNINativeMethod;

我们应用开发中使用的 MediaRecorder就是采用动态注册的方式,我们来看看其JNI层是怎么来使用这个 ​​JNINativeMethod​​的:

// android_media_MediaRecorder.cpp
static const JNINativeMethod gMethods[] = {
...
{"start", "()V", (void *)android_media_MediaRecorder_start},
{"stop", "()V", (void *)android_media_MediaRecorder_stop},
{"pause", "()V", (void *)android_media_MediaRecorder_pause},
{"resume", "()V", (void *)android_media_MediaRecorder_resume},
{"native_reset", "()V", (void *)android_media_MediaRecorder_native_reset},
{"release", "()V", (void *)android_media_MediaRecorder_release},
{"native_init", "()V", (void *)android_media_MediaRecorder_native_init}...
};

在这个JNI文件中,定义了一个 ​​JNINativeMethod​​​数组,里面声明了这些关联的方法
比如最后一行的:
​​​"native_init"​​​:在Java中的方法
​​​"()v"​​​ :Java层的签名
​​​"android_media_MediaRecorder_native_init"​​ :为对应JNI层的函数。

这里只声明一个 ​​JNINativeMethod​​​数组并不就完全的注册了这些函数,这个类还要去编写一个注册函数才有用,这个类中定义了一个函数来完成这个任务,它就是 ​​register_android_media_MediaRecorder()​​:

// android_media_MediaRecorder.cpp
// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaRecorder(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaRecorder", gMethods, NELEM(gMethods));
}

从英文注释中,可以看出这个方法用来注册native方法,并且它只在 ​​JNI_OnLoad()​​​中被调用,这个方法我们听名字就知道,肯定是在 ​​System.loadLibrary()​​​会调用其里面的这个方法。因为在多媒体框架中都要进行 ​​JNINativeMethod​​​数组的注册,因此,注册函数就统一放在 ​​JNI_OnLoad()​​​中。
也就是说 ​​​JNI_OnLoad()​​​是在 ​​android_mediaMediaPlayer.cpp​​中的,我们来看看这个方法:

// android_media_MediaPlayer.cpp
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
JNIEnv* env = NULL;
jint result = -1;

if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);
...
// 1
if (register_android_media_MediaRecorder(env) < 0) {
ALOGE("ERROR: MediaRecorder native registration failed\n");
goto bail;
}
...
result = JNI_VERSION_1_4;
bail:
return result;
}

在​​JNI_OnLoad()​​​中调用了整个多媒体框架的注册 ​​JNINativeMethod()​​​数组的函数。
除了上面说到的 MediaRecorder,还有我们熟知的 MediaPlayer、MediaScanner等框架,都放在这里。
在注释1中就是调用了 ​​​register_android_media_MediaRecorder()​​。

我们回到 ​​register_android_media_MediaRecorder()​​​中,它就是执行了 ​​AndroidRuntime::registerNativeMethod()​​这个方法,我们看看这个方法:

// AndroidRuntime.cpp
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

它又返回了 ​​jniRegisterNativeMethods()​​​,它被定义在 ​​JNIHelp.cpp​​中:

// JNIHelp.cpp
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
...
// 1
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
char* tmp;
const char* msg;
if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
msg = "RegisterNatives failed; aborting...";
} else {
msg = tmp;
}
e->FatalError(msg);
}
return 0;
}

注释1可以看出,最终通过调用​​JNIEnv.RegisterNatives()​​来完成JNI的注册。后面就不讲了,因为会扯到JNIEnv,等了解了JNIEnv,再去看这些代码也不迟。

动态注册总结:
通过声明一个 ​​​JNINativeMethod​​数组,来把对应的函数名称放到这个数组中,通过JNIEnv的的注册,来实现函数的一一对应,这样做相比于静态注册:

  • 直观明了
    想要注册的方法全都放在一个数组里,统一管理并且一目了然
  • 效率高
    指针在一开始就指向了对应的函数,这样加快了效率。

2.数据类型的转换

我们依旧拿上面 MediaRecorder为例,看下 ​​android_media_MediaRecorder_start()​​:

// adnroid_media_MediaRecorder.cpp
static void
android_media_MediaPlayer_start(JNIEnv *env, jobject thiz)
{
....
}

​android_media_MediaRecorder_start()​​​ 的第二个参数 ​​jobject​​​,我们不是很清楚它是做什么的。
通过JNI,我们在Java层的数据经过了转换,变成了我们看不懂的数据。

我们需要了解JNI中的 ​​jint​​​、​​jstring​​​、​​jclass​​这些数据类型是啥,我们就要搞清楚它们是这么来的。Java数据类型分成基本类型和引用类型,JNI也分成了这两个部分。

2. 1 JNI中的基本数据类型

基本数据类型的转换如下表所示,可以看到除了 void,其他数据类型只在前面加一个“j”,第三列是签名格式。之前在 ​​JNINativeMethod​​中出现过,它就是一个类型的简写:

Java

Native

Signature

byte

jbyte

B

char

cchar

C

double

jdouble

D

float

jfloat

F

int

jint

I

short

jshort

S

long

jlong

J

boolean

jboolean

Z

void

void

V

2. 2 JNI中的引用数据类型

数组的JNI层数据都以Array结尾,签名都以“[”开头,有些数据类型以“;”结尾:

Java

Native

Signature

所有对象Object

jobject

L+classname +;

Class

jclass

Ljava/lang/Class;

Throwable

jthrowable

Ljava/lang/Throwable;

String

jstring

Ljava/lang/String

Object[]

jobject[]Array

[L+classname +;

byte[]

jbyteArray

[B

char[]

jcharArray

[C

double[]

jdoubleArray

[D


他们也有继承关系,比如所有的类型都继承 ​​​jobject​​​,所有的数组都继承自 ​​jarray​​。

再来看一个MediaRecorder的例子:

// MediaRecorder.java
private native void _setOutputFile(FileDescriptor fd) throws IllegalStateException, IOException;

// android_media_MediaRecorder.cpp
android_media_MediaRecorder_setNextOutputFileFD(JNIEnv *env, jobject thiz, jobject fileDescriptor)
{
...
}

可以看到 ​​FileDescriptor​​​就转成了 ​​jobject​

3.方法签名

方法签名的意义是什么:便于注册后找到对应的函数
因为Java中有重载机制,即同返回类型+同类名,如果使用动态注册,在​​​JNINativeMethod​​数组中,只放上两个名字(JNI层和JAVA层函数名),到时候如果有重载函数,JNI指针是无法区分的。

这个时候就出现了方法签名, 它会带上参数,比如 ​​(jlong)V​​​,就是输入的是一个long型,返回的是一个void。
这样即使重载,也能通过参数来区分。这就是方法签名的意义。

Java中用 ​​javap​​来自动生成签名,这里就不再演示了。

4.JNIEnv

JNIEnv是只有JNI才有的,它是Java环境的代表,通过 ​​JNIEnv *​​​指针,就可以在Native层中访问Java层的代码。
它是线程私有的,so每个线程都有自己的 ​​​JNIEnv *​​,它的作用有以下两点:

  1. 调用Java的方法
  2. 操作Java中的变量和对象

先来看下​​JNIEnv​​的定义,它在jni.h中被声明:

// jni.h
#if defined(__cplusplus)
//1 c++中JNIEnv类型
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
//2 C中JNIEnv类型
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;

首先先判断当前是什么语言,​​_cplusplus​​​就是C++,如果当前语言是C++,JNIEnv就是 ​​_JNIEnv​​​类型,如果是C,就是 ​​JNIInvokeInterface*​​​这个类型。
这里的定义中我们也看到了JVM,​​​JavaVM​​​就是JVM在Native层的代表。通过 ​​JavaVM.AttachCurrentThread()​​可以获取这个线程的JNIEnv,这样就可以在不同的线程调用Java方法了。

我们来看下 ​​_JNIEnv​​这个C++中定义的结构体:

// jni.h
struct _JNIEnv {
#if defined(__cplusplus)
...
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
...
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }
...
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }
...
}

这里定义了很多的函数,之前讲过的动态注册就在这里面。
这里列出了三个常用的方法:

  1. FindClass
    用来找到Java中指定名称的类
  2. GetMethodID
    用来得到Java中的方法
  3. GetFieldID
    用来得到Java中的成员变量。

这些函数都调用了 ​​JNINativeInterface​​​的方法,而​​JNINativeInterface​​对每个函数都保存了一个指针,这样就能够定位到虚拟机中的JNI函数表,从而实现了JNI层在虚拟机中的函数调用了。这样JNI就可以访问Java层的代码了。

4.1 jfieldID和jmethodID

再来看看 ​​native_init()​​中的方法:

static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;

clazz = env->FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
return;
}

fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
if (fields.context == NULL) {
return;
}

fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");
if (fields.surface == NULL) {
return;
}

jclass surface = env->FindClass("android/view/Surface");
if (surface == NULL) {
return;
}

fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
"(Ljava/lang/Object;IIILjava/lang/Object;)V");
if (fields.post_event == NULL) {
return;
}
}

struct fields_t{
jfieldID context;
jfieldID surface;
jmethodID post_event;
}
static

可以看到通过 ​​env->FindClass​​​来找到MediaRecorder的Java类,并赋值给 clazz,clazz就是Java层的MediaRecorder在JNI层的代表。
再用 ​​​env->GetFieldID​​​获取类的成员变量 “mNativeContext”、“mSurface”
最后再通过 ​​​env->GetStaticMethodID​​​获取类的静态成员方法 “​​postEventFromNative()​​​” 。并赋值给 fields的post_event属性。
fields是 ​​​fields_t​​​类型的对象,上面中它存放了这几个要查找的成员变量和方法。
等赋值了之后,之后就不用再去查找了,直接调用这些赋值过的对象就行了,节省了再查询的效率。

4.2 使用jfieldID和jmethodID

保存了jfieldID、jmethodID类型的变量,接着怎么使用他们呢。看看下面的方法:

// android_media_MediaRecorder.cpp
void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
{
ALOGV("JNIMediaRecorderListener::notify");
JNIEnv *env = AndroidRuntime::getJNIEnv();
// 1
env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);
}

在注释1:中调用了 ​​env->CallStaticVoidMethod()​​​,这里面就传入了 fields.post_event,我们来看下Java中的这个方法,就是上一节中传入到post_event中的静态方法 ​​postEventFromNative()​​:

// MediaRecorder.java
private static void postEventFromNative(Object mediarecorder_ref,
int what, int arg1, int arg2, Object obj)
{
MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get();
if (mr == null) {
return;
}
if (mr.mEventHandler != null) {
// 1
Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);
mr.mEventHandler.sendMessage(m);
}
}

这个方法主要就是创建一个消息,注释1中,创建消息后发送给 mEventHandler,就是把代码逻辑转移到主线程中去。
​​​env->CallStaticVoidMethod()​​​会调用Java层的 ​​postEventFromNative()​​​。
当然了如果调用 ​​​env->CallVoidMEthod()​​​就是调用Java层的非静态方法了。
​​​jfieldID​​的使用也是类似的,这里不多做讲解。

5.引用类型

Java有四大引用类型,强、弱、软、虚。
JNI也有,分别是 本地引用全局引用弱全局引用

5.1 本地引用

JNIEnv提供的函数所返回的引用基本上都是本地引用,因此本地引用也是JNI最常见的类型。
本地引用的特点如下:

  • 当Native函数返回时,这个本地引用就会被自动释放
  • 只在创建它的线程中有效,不能跨线程传递,属于线程私有
  • 局部引用是JVM负责的引用类型,受JVM管理

比如之前的 ​​native_init()​​中:

// android_media_MediaRecorder
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;

clazz = env->FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
return;
}
....

上面的 clazz就是本地引用,在 ​​native_init()​​​返回后会被自动释放。
我们也可以调用 ​​​JNIEnv->DeleteLocalRef​​来手动删除本地引用。

5.2 全局引用

全局引用的特点和本地引用相反:

  • 它可以跨线程使用
  • 全局引用不受JVM管理
  • 在native函数返回时并不会被自动释放,因此这些引用都需要手动来释放,并且不会被GC回收。

这些引用一般都在析构函数中 通过 ​​JNIEnv->DeleteGlobalRef()​​来手动释放

5.3 弱全局引用

弱全局是一种特殊的全局引用,它和全局引用有相似之处。
不同的是 弱全局引用可以被GC回收,被释放了之后,对象仍然存在,只是指向为bull。
​​​JNIEnv->NewWeakGlobalRef()​​用来创建一个弱全局引用。

由于弱全局引用会被GC,所以在每次使用弱引用之前,都要先判断其有没有被回收,不然可能会出现空指针异常,用来判断是否回收的方法是 ​​JNIEnv->IsSameObject()​​,下面是一个使用的正确姿势:

if(env->IsSameObject(weakGlobalRef, NULL)) {
return false;
}
...使用弱全局引用

在上面的代码中,将我们要使用的弱全局变量和null进行比较判断是否为相同的类,如果被回收则返回false。
(上面的判断代码其实可以写成 weakGlobalRef == null),这样写可以更加了解这个方法的作用。

到这里JNI的一些基本原理就讲完了,针对于应用开发这些知识点貌似就足够了。