这几天来学习下JNI的知识,参考的文章有:
《Android进阶解密》第九章JNI原理
JAVA基础之理解JNI原理
JNI是Java Native Interface的缩写,翻译是Java本地接口。它的作用是来桥接Java和其他语言(C/C++),完成交互工作。Android中的语言层可以分成Java层和Native层。为什么会有Native层呢?是因为在Java出现之前,很多的功能和系统都是由Native语言写的,所以Java出现之后就不必重复造轮子,直接调用这些方法就行了,而且Native层更接近汇编语言,所以性能更优。
所以代码中就出现了Java层调用Native层的方法,而这个跨语言的调用过程就是 JNI,下图就是他们的关系:
当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,来看下下图中的关系:
在MediaRecorder框架中,Java Framework层对应的是MediaRecorder.java,是我们应用开发中直接调用的类。
JNI层中对应的是 libmedia_jni.so,如果一个so库的名称结尾带有 _jni,则说明它是一个JNI的动态库。
Native层中对应的是 libmedia.so,它是真正实现业务的类。
1.1 Java Framework层的 MediaRecorder
我们先来查看MediaRecorder.java的源码,看看在Java层是如何使用JNI的:
AI检测代码解析
// 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
静态代码是最先加载的。
注释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方法:
AI检测代码解析
// 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)
{
...
}
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
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编写:
接着我们对MediaRecorder.java进行编译和生成JNI方法,我们进入 media/src/main/java中,在 terminal命令行中输入:
AI检测代码解析
javac com/rikkatheworld/media/MediaRecorder.java
javah -jni com.rikkatheworld.media.MediaRecorder
1.
2.
最后会产生了一个 MediaReorder.class和一个 com_rikkatheworld_media_MediaRecorder.h头文件。
上面可能这里可能会有点坑,可以看我 Android音视频开发入门(5)使用LAME编码一个PCM文件第一节怎么去搭建一个JNI项目,这里我就不多赘述。
我们来看下这个被编译出来的头文件的内容:
AI检测代码解析
// 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
}
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
被编译出来的头文件就是我们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中的源码是这样的:
AI检测代码解析
// jni.h
typedef struct {
const char* name; //Java方法中的名字
const char* signature; //Java方法的简明信息
void* fnPtr; //JNI中对应的方法指针
} JNINativeMethod;
1.
2.
3.
4.
5.
6.
我们应用开发中使用的 MediaRecorder就是采用动态注册的方式,我们来看看其JNI层是怎么来使用这个 JNINativeMethod的:
AI检测代码解析
// 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}...
};
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
在这个JNI文件中,定义了一个 JNINativeMethod数组,里面声明了这些关联的方法
比如最后一行的:
"native_init":在Java中的方法
"()v" :Java层的签名
"android_media_MediaRecorder_native_init" :为对应JNI层的函数。
这里只声明一个 JNINativeMethod数组并不就完全的注册了这些函数,这个类还要去编写一个注册函数才有用,这个类中定义了一个函数来完成这个任务,它就是 register_android_media_MediaRecorder():
AI检测代码解析
// 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));
}
1.
2.
3.
4.
5.
6.
7.
8.
从英文注释中,可以看出这个方法用来注册native方法,并且它只在 JNI_OnLoad()中被调用,这个方法我们听名字就知道,肯定是在 System.loadLibrary()会调用其里面的这个方法。因为在多媒体框架中都要进行 JNINativeMethod数组的注册,因此,注册函数就统一放在 JNI_OnLoad()中。
也就是说 JNI_OnLoad()是在 android_mediaMediaPlayer.cpp中的,我们来看看这个方法:
AI检测代码解析
// 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;
}
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
在JNI_OnLoad()中调用了整个多媒体框架的注册 JNINativeMethod()数组的函数。
除了上面说到的 MediaRecorder,还有我们熟知的 MediaPlayer、MediaScanner等框架,都放在这里。
在注释1中就是调用了 register_android_media_MediaRecorder()。
我们回到 register_android_media_MediaRecorder()中,它就是执行了 AndroidRuntime::registerNativeMethod()这个方法,我们看看这个方法:
AI检测代码解析
// AndroidRuntime.cpp
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
1.
2.
3.
4.
5.
6.
它又返回了 jniRegisterNativeMethods(),它被定义在 JNIHelp.cpp中:
AI检测代码解析
// 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
注释1可以看出,最终通过调用JNIEnv.RegisterNatives()来完成JNI的注册。后面就不讲了,因为会扯到JNIEnv,等了解了JNIEnv,再去看这些代码也不迟。
动态注册总结:
通过声明一个 JNINativeMethod数组,来把对应的函数名称放到这个数组中,通过JNIEnv的的注册,来实现函数的一一对应,这样做相比于静态注册:
直观明了
想要注册的方法全都放在一个数组里,统一管理并且一目了然
效率高
指针在一开始就指向了对应的函数,这样加快了效率。
2.数据类型的转换
我们依旧拿上面 MediaRecorder为例,看下 android_media_MediaRecorder_start():
AI检测代码解析
// adnroid_media_MediaRecorder.cpp
static void
android_media_MediaPlayer_start(JNIEnv *env, jobject thiz)
{
....
}
1.
2.
3.
4.
5.
6.
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的例子:
AI检测代码解析
// 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)
{
...
}
1.
2.
3.
4.
5.
6.
7.
8.
可以看到 FileDescriptor就转成了 jobject
3.方法签名
方法签名的意义是什么:便于注册后找到对应的函数。
因为Java中有重载机制,即同返回类型+同类名,如果使用动态注册,在JNINativeMethod数组中,只放上两个名字(JNI层和JAVA层函数名),到时候如果有重载函数,JNI指针是无法区分的。
这个时候就出现了方法签名, 它会带上参数,比如 (jlong)V,就是输入的是一个long型,返回的是一个void。
这样即使重载,也能通过参数来区分。这就是方法签名的意义。
Java中用 javap来自动生成签名,这里就不再演示了。
4.JNIEnv
JNIEnv是只有JNI才有的,它是Java环境的代表,通过 JNIEnv *指针,就可以在Native层中访问Java层的代码。
它是线程私有的,so每个线程都有自己的 JNIEnv *,它的作用有以下两点:
调用Java的方法
操作Java中的变量和对象
先来看下JNIEnv的定义,它在jni.h中被声明:
AI检测代码解析
// 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;
1.
2.
3.
4.
5.
6.
7.
8.
9.
首先先判断当前是什么语言,_cplusplus就是C++,如果当前语言是C++,JNIEnv就是 _JNIEnv类型,如果是C,就是 JNIInvokeInterface*这个类型。
这里的定义中我们也看到了JVM,JavaVM就是JVM在Native层的代表。通过 JavaVM.AttachCurrentThread()可以获取这个线程的JNIEnv,这样就可以在不同的线程调用Java方法了。
我们来看下 _JNIEnv这个C++中定义的结构体:
AI检测代码解析
// 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
这里定义了很多的函数,之前讲过的动态注册就在这里面。
这里列出了三个常用的方法:
FindClass
用来找到Java中指定名称的类
GetMethodID
用来得到Java中的方法
GetFieldID
用来得到Java中的成员变量。
这些函数都调用了 JNINativeInterface的方法,而JNINativeInterface对每个函数都保存了一个指针,这样就能够定位到虚拟机中的JNI函数表,从而实现了JNI层在虚拟机中的函数调用了。这样JNI就可以访问Java层的代码了。
4.1 jfieldID和jmethodID
再来看看 native_init()中的方法:
AI检测代码解析
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
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
可以看到通过 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类型的变量,接着怎么使用他们呢。看看下面的方法:
AI检测代码解析
// 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.
2.
3.
4.
5.
6.
7.
8.
在注释1:中调用了 env->CallStaticVoidMethod(),这里面就传入了 fields.post_event,我们来看下Java中的这个方法,就是上一节中传入到post_event中的静态方法 postEventFromNative():
AI检测代码解析
// 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
这个方法主要就是创建一个消息,注释1中,创建消息后发送给 mEventHandler,就是把代码逻辑转移到主线程中去。
env->CallStaticVoidMethod()会调用Java层的 postEventFromNative()。
当然了如果调用 env->CallVoidMEthod()就是调用Java层的非静态方法了。
jfieldID的使用也是类似的,这里不多做讲解。
5.引用类型
Java有四大引用类型,强、弱、软、虚。
JNI也有,分别是 本地引用、全局引用、弱全局引用。
5.1 本地引用
JNIEnv提供的函数所返回的引用基本上都是本地引用,因此本地引用也是JNI最常见的类型。
本地引用的特点如下:
当Native函数返回时,这个本地引用就会被自动释放
只在创建它的线程中有效,不能跨线程传递,属于线程私有
局部引用是JVM负责的引用类型,受JVM管理
比如之前的 native_init()中:
AI检测代码解析
// android_media_MediaRecorder
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
return;
}
....
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
上面的 clazz就是本地引用,在 native_init()返回后会被自动释放。
我们也可以调用 JNIEnv->DeleteLocalRef来手动删除本地引用。
5.2 全局引用
全局引用的特点和本地引用相反:
它可以跨线程使用
全局引用不受JVM管理
在native函数返回时并不会被自动释放,因此这些引用都需要手动来释放,并且不会被GC回收。
这些引用一般都在析构函数中 通过 JNIEnv->DeleteGlobalRef()来手动释放
5.3 弱全局引用
弱全局是一种特殊的全局引用,它和全局引用有相似之处。
不同的是 弱全局引用可以被GC回收,被释放了之后,对象仍然存在,只是指向为bull。
JNIEnv->NewWeakGlobalRef()用来创建一个弱全局引用。
由于弱全局引用会被GC,所以在每次使用弱引用之前,都要先判断其有没有被回收,不然可能会出现空指针异常,用来判断是否回收的方法是 JNIEnv->IsSameObject(),下面是一个使用的正确姿势:
AI检测代码解析
if(env->IsSameObject(weakGlobalRef, NULL)) {
return false;
}
...使用弱全局引用
1.
2.
3.
4.
在上面的代码中,将我们要使用的弱全局变量和null进行比较判断是否为相同的类,如果被回收则返回false。
(上面的判断代码其实可以写成 weakGlobalRef == null),这样写可以更加了解这个方法的作用。
到这里JNI的一些基本原理就讲完了,针对于应用开发这些知识点貌似就足够了。
链接:https://blog.51cto.com/u_15719342/5475254