framework | 一文搞定JNI原理
0.前言
在系统进程介绍中,我们在分析源码时,经常能看到Java层代码和Native层代码互相调用,而实现这个的技术就是JNI技术。
JNI是Java Native Interface的缩写,翻译为Java本地接口,是Java与其他语言通信的桥梁。当出现一些用Java无法处理的任务时,就需要用到JNI技术,一般有如下情况:
- 需要调用Java语言不支持的依赖于操作系统平台特性的一些功能。比如调用linux系统某个功能,该功能Java不支持。
- 为了整合一些以前的非Java语言开发的系统。比如用到早期实现的C/C++语言开发的一些功能或系统,将这些功能整合到当前的系统中。
- 为了节省程序的运行时间,必须采用其他语言(比如C/C++)来提升运行效率。比如游戏、音视频开发涉及的音视频编码和图像绘制需要更快的处理速度。
本篇文章我们主要以系统源码中用到的JNI为引子,来分析JNI注册、如何找到JNI方法、JNI原理等知识。
1.正文
Android系统按语言来划分的话由俩个世界组成,分别是Java世界和Native世界,至于为什么要有Native,在我们之前分析Android架构就知道了,Android系统中有大量的库是由C/C++语言写的,位于Native层。
连通的桥梁就是JNI,JNI的作用用跟浅显的话来翻译就是中间人,在Java代码中想调用一个C++代码,Java层就需要告诉JNI这个中间人,然后JNI去Native层找到这个C++方法,执行完,把结果再通过JNI告诉Java层。所以JNI的实现,必须是虚拟机和Android系统共同实现的,相当于把请求在中间转了一手。
1.1 MediaRecorder中的JNI
MediaRecoder我们并不陌生,它是用来录音和录像的,这里不会介绍其具体功能,而是重点介绍里面用到的JNI框架。这里先给出框架图:
这里MediaRecorder.java就是我们平时使用的Java类,JNI层对应的是libmedia_jni.so,它是一个JNI的动态库,而Native层对应的是libmedia.so,这个动态库完成了实际的调用功能。
上面这段话说起来容易,但是对于C/C++不熟悉的话会不知道这个动态库.so到底是个啥,不易理解。这里对so做个简单介绍,当我们编写的C++代码经过编译就可以生成.so文件,这是一个二进制文件,经过链接就可以运行了,所以这里暂且认为so动态库就是C++代码的可运行形态(至于为什么是动态库,还有链接的问题,后面会有单独文章分析)。
然后上面架构图就可以变成下面这样:
这里就又有个问题,为什么.java文件直接调用最下层的.cpp不就可以了,为什么中间又一层.cpp代码呢?这也就是JNI层的表现形式,中间的JNI层其实也是C++代码,它的作用就是桥梁,在这个C++代码中我们可以调用Java层代码也可以调用Native层的C++代码(天然可以,都是C++)。由于这里特殊的C++代码,可以把俩种混调用(Java和C++),所以JNI有它自己的类型。
好了,现在我们知道在这个JNI的C++代码中可以调用Native中的C++代码,还有一个问题需要解决,就是我在Java代码中定义一个叫做native init()函数,该函数在对应在JNI层中C++代码的哪一个呢?是不是解决这个问题,这整个链路就通了,接下来我们就来根据源码分析。
1.2 Java层的MediaRecorder
先来看看Java层的.java代码:
/frameworks/base/media/java/android/media/MediaRecorder.java
public class MediaRecorder
80 {
81 static {
82 System.loadLibrary("media_jni");
83 native_init();
84 }
...
private static native final void native_init();
...
}
这里先加载了名为"media_jni"的动态库,也就是libmedia_jni.so,然后调用native_init()方法,而该方法是用native修饰的,说明它是一个native方法,由JNI来实现。
在Java层需要的做法比较简单,就是申明native方法,然后把实现该native方法的C/C++代码(JNI层)通过System.loadLibrary加载进来(加载的是so,其实从前面知道就是C/C++代码)。
1.3 JNI层的MediaRecorder
MediaRecoder的JNI层由android_media_record.cpp实现,刚刚上面定义的native_init方法定义如下:
/frameworks/base/media/jni/android_media_MediaRecorder.cpp
static void android_media_MediaRecorder_native_init(JNIEnv *env)
540 {
541 jclass clazz;
542
543 clazz = env->FindClass("android/media/MediaRecorder");
544 if (clazz == NULL) {
545 return;
546 }
547
548 fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
549 if (fields.context == NULL) {
550 return;
551 }
552
553 fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");
554 if (fields.surface == NULL) {
555 return;
556 }
557
558 jclass surface = env->FindClass("android/view/Surface");
559 if (surface == NULL) {
560 return;
561 }
562
563 fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
564 "(Ljava/lang/Object;IIILjava/lang/Object;)V");
565 if (fields.post_event == NULL) {
566 return;
567 }
568 }
这里的android_media_MediaRecorder_native_init方法就是Java层native_init方法在JNI层的实现,在Java代码中调用native_init方法,就会调用该方法。
那这2个方法的对应关系是如何确定的呢?就相当于这个文件的C++代码和前面所说的Java代码都加载到了内存,Java文件里调用native_init方法,是如何找到C++文件中对应的方法,这个就需要了解JNI方法注册的知识。
1.4 Native方法注册
Native方法注册分为静态注册和动态注册,其中静态注册多用于NDK开发,而动态注册多用于Framework开发,下面我们就分别介绍。
1.4.1 静态注册
这里我们直接仿照Framework的代码,创建一个MediaRecorder.java类,如下:
package com.wayeal.module;
public class MediaRecorder {
static {
System.loadLibrary("media_jni");
native_init();
}
private static native final void native_init();
}
然后使用javah命令可以生成该Java文件的.h头文件,这里关于javah的命令执行起来有个小细节,我这里如下:
D:\AllAndroidProject\wayealProject\GuanWangPaiShui\app\src\main\java> javah -jni com.wayeal.module.MediaRecorder
虽然我这里的MediaRecorder.java是在com.wayeal.module下面,但是执行该javah命令时,需要在java目录,并且后面跟上类的全名,执行完javah命令后,会在java目录生成一个com_wayeal_module_MediaRecorder.h文件,内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_wayeal_module_MediaRecorder */
#ifndef _Included_com_wayeal_module_MediaRecorder
#define _Included_com_wayeal_module_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_wayeal_module_MediaRecorder
* Method: native_init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_wayeal_module_MediaRecorder_native_1init
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
至于为什么会生成这个文件?那是因为javah命令就是专门JNI为.java文件生成对应的.h文件所设计的,关于该文件有如下注意事项:
- 什么是.h文件?即头文件,是运用在C/C++中的一种文件,在头文件中定义函数,然后在.c/.cpp中具体实现,所以可以看成是一种声明。
- 这个文件是生成文件,最好不要修改,即可以直接把该.h文件拷贝到C++项目中,对其中的代码进行实现。
- 这里可以发现生成的文件和方法都是很有规律的。首先是文件名,即把原来Java全类名:com.wayeal.module.MediaRecorder中的.换成下划线_得到头文件名:com_wayeal_module_MediaRecorder.h;其次是在Java中定义的native_init方法,在头文件中以"Java"+全类名+方法名的格式生成,具体为:Java_com_wayeal_module_MediaRecorder_native_1init,这里多了一个"1",这时因为Java方法中的_被换成了_1。
- 方法的参数是JNIEnv指针,其中JNIEnv是重点,它代表Native世界中Java环境的代表,通过JNIEnv* 指针可以在Native世界访问Java世界的代码,进行操作,它只在创建它的线程中有效,不能跨线程传递。
- 这里的jclass是JNI的数据类型,对应Java中的Class类型,关于JNI类型的出现我们前面分析过了,至于具体的JNI类型,后面细说。
由此可见,通过javah命令,就建立了Java和C++的联系。在Java代码中调用native_init方法,就会从JNI中寻找Java_com_wayeal_module_MediaRecorder_native_1init方法,其实就是在JNI中找到并且保存该函数的函数指针,保存是为了下次再调用时,可以快速找到。
所以静态注册其实就是根据方法名,将Java方法和JNI函数建立关联,缺点很明显,如下:
- JNI层的函数名非常长。
- 声明Native方法的类需要用javah生成头文件,当Java文件改变时,需要多次调用javah方法。
- 初次调用native方法时,JNI框架需要找函数,建立关联,会影响效率。
1.4.2 动态注册
分析一下前面的静态注册,其实生成.h文件就是为了建立统一的命名规则,然后JNI框架方便把Java层中的native方法和具体实现的地方给对应起来,而这个对应关系其实就相当于是保存一个键值对,键就是Java中的native方法,值就是C++方法的函数指针。
既然静态注册这么麻烦,那能不能直接把Java方法和C++方法建立关联呢?动态注册就是这样做的。
首先就是这种Java方法和C++方法关联关系的定义,是使用JNINativeMethod结构体定义,如下:
/development/ndk/platforms/android-9/include/jni.h
typedef struct {
144 const char* name;
145 const char* signature;
146 void* fnPtr;
147 } JNINativeMethod;
这里分别表示Java方法的名字、Java方法的签名信息和JNI中对应的方法指针。
前面说了,动态注册常用于Framework开发,比如系统的MediaRecoder就是的,我们来看其JNI层是如何定义的。
在JNI的C++代码中有一个JNINativeMethod类型的数组,如下:
/frameworks/base/media/jni/android_media_MediaRecorder.cpp
static const JNINativeMethod gMethods[] = {
663 {"setCamera", "(Landroid/hardware/Camera;)V", (void *)android_media_MediaRecorder_setCamera},
664 {"setVideoSource", "(I)V", (void *)android_media_MediaRecorder_setVideoSource},
665 {"setAudioSource", "(I)V", (void *)android_media_MediaRecorder_setAudioSource},
666 {"setOutputFormat", "(I)V", (void *)android_media_MediaRecorder_setOutputFormat},
667 {"setVideoEncoder", "(I)V", (void *)android_media_MediaRecorder_setVideoEncoder},
668 {"setAudioEncoder", "(I)V", (void *)android_media_MediaRecorder_setAudioEncoder},
669 {"setParameter", "(Ljava/lang/String;)V", (void *)android_media_MediaRecorder_setParameter},
670 {"_setOutputFile", "(Ljava/io/FileDescriptor;)V", (void *)android_media_MediaRecorder_setOutputFileFD},
671 {"_setNextOutputFile", "(Ljava/io/FileDescriptor;)V", (void *)android_media_MediaRecorder_setNextOutputFileFD},
672 {"setVideoSize", "(II)V", (void *)android_media_MediaRecorder_setVideoSize},
673 {"setVideoFrameRate", "(I)V", (void *)android_media_MediaRecorder_setVideoFrameRate},
674 {"setMaxDuration", "(I)V", (void *)android_media_MediaRecorder_setMaxDuration},
675 {"setMaxFileSize", "(J)V", (void *)android_media_MediaRecorder_setMaxFileSize},
676 {"_prepare", "()V", (void *)android_media_MediaRecorder_prepare},
677 {"getSurface", "()Landroid/view/Surface;", (void *)android_media_MediaRecorder_getSurface},
678 {"getMaxAmplitude", "()I", (void *)android_media_MediaRecorder_native_getMaxAmplitude},
679 {"start", "()V", (void *)android_media_MediaRecorder_start},
680 {"stop", "()V", (void *)android_media_MediaRecorder_stop},
681 {"pause", "()V", (void *)android_media_MediaRecorder_pause},
682 {"resume", "()V", (void *)android_media_MediaRecorder_resume},
683 {"native_reset", "()V", (void *)android_media_MediaRecorder_native_reset},
684 {"release", "()V", (void *)android_media_MediaRecorder_release},
685 {"native_init", "()V", (void *)android_media_MediaRecorder_native_init},
686 {"native_setup", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V",
687 (void *)android_media_MediaRecorder_native_setup},
688 {"native_finalize", "()V", (void *)android_media_MediaRecorder_native_finalize},
689 {"native_setInputSurface", "(Landroid/view/Surface;)V", (void *)android_media_MediaRecorder_setInputSurface },
690
691 {"native_getMetrics", "()Landroid/os/PersistableBundle;", (void *)android_media_MediaRecorder_native_getMetrics},
692 };
在这其中我们发现前面Java中所说的native_init方法也有定义,其方法签名是"()V",这个方法签名简单来说就是Java方法可以重载,仅靠名字是无法区分的,方法签名就是方法的参数和返回值的合体,这里就说明native_init这个Java方法的参数是没有,返回值是void,关于方法签名,后面细说,然后就是指明了对应的C++函数的函数指针。
有了方法关系还不行,需要把这个关系列表告诉给JNI,即需要注册它,注册的函数是register_android_media_MediaRecorder:
/frameworks/base/media/jni/android_media_MediaRecorder.cpp
int register_android_media_MediaRecorder(JNIEnv *env)
697 {
698 return AndroidRuntime::registerNativeMethods(env,
699 "android/media/MediaRecorder", gMethods, NELEM(gMethods));
700 }
在该方法中会调用AndroidRuntime这个C++类的方法,这里我们就知道了保存和查询该关系的就是虚拟机和运行时环境所做的,待会再分析该函数。
那么问题来了,既然这个方法是注册关系,那这个方法是什么时候调用呢?从该方法注释我们可以知道,它会在android_media_MediaPlayer.cpp的JNI_OnLoad中调用:
/frameworks/base/media/jni/android_media_MediaPlayer.cpp
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
1465 {
1466 JNIEnv* env = NULL;
1467 jint result = -1;
1468
1469 ...
1490 if (register_android_media_MediaRecorder(env) < 0) {
1491 ALOGE("ERROR: MediaRecorder native registration failed\n");
1492 goto bail;
1493 }
...
1574
1575 /* success -- return valid version number */
1576 result = JNI_VERSION_1_4;
1577
1578 bail:
1579 return result;
1580 }
那这个JNI_OnLoad会在什么调用呢,该方法会在我们最开始的System.loadLibrary()函数中调用,这么一看,整个链路就通了。
我们来总结画个时序图:
从上面时序图我们发现JNI的原理实现是虚拟机、AndroidRuntime共同实现的。
1.5 JNI查找方式
了解了Java动态注册,我们来回想一下之前Android系统启动分析,首先是用户态的第一个进程init,然后fork出一个横穿Java和C/C++的Zygote进程,在Zygote进程中会创建虚拟机,在创建完虚拟机后会调用starReg来完成虚拟机中JNI方法注册,这也就是为什么创建完虚拟机,可以使用Java代码,而Java代码中可以使用native方法的原因。
1.5.1 注册JNI方法
在Zygote中会调用AndroidRuntime::startReg来注册:
/frameworks/base/core/jni/AndroidRuntime.cpp
int AndroidRuntime::startReg(JNIEnv* env)
1420 {
...
1437 env->PushLocalFrame(200);
//进行注册,gRegJNI保存着待注册的方法
1439 if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
1440 env->PopLocalFrame(NULL);
1441 return -1;
1442 }
1443 env->PopLocalFrame(NULL);
1444
1445 //createJavaThread("fubar", quickTest, (void*) "hello");
1446
1447 return 0;
1448 }
这里我们来看一下这个gRegJNI数组:
/frameworks/base/core/jni/AndroidRuntime.cpp
static const RegJNIRec gRegJNI[] = {
1264 REG_JNI(register_com_android_internal_os_RuntimeInit),
1265 ...
1279 REG_JNI(register_android_os_Process),
1280 REG_JNI(register_android_os_SystemProperties),
1281 REG_JNI(register_android_os_Binder),
1282 REG_JNI(register_android_os_Parcel),
1283 REG_JNI(register_android_nio_utils),
1284 ...
1353 REG_JNI(register_android_os_MessageQueue),
1354 ...
1413
1414 };
这里会注册几百个不同文件的JNI方法,这里举例我们非常熟悉的MessageQueue,看一下:
/frameworks/base/core/jni/android_os_MessageQueue.cpp
int register_android_os_MessageQueue(JNIEnv* env) {
224 int res = RegisterMethodsOrDie(env, "android/os/MessageQueue", gMessageQueueMethods,
225 NELEM(gMessageQueueMethods));
226
227 jclass clazz = FindClassOrDie(env, "android/os/MessageQueue");
228 gMessageQueueClassInfo.mPtr = GetFieldIDOrDie(env, clazz, "mPtr", "J");
229 gMessageQueueClassInfo.dispatchEvents = GetMethodIDOrDie(env, clazz,
230 "dispatchEvents", "(II)I");
231
232 return res;
233 }
这里的RegisterMethodsOrDie就是用来注册方法,gMessageQueueMethods就是需要注册的方法:
/frameworks/base/core/jni/android_os_MessageQueue.cpp
static const JNINativeMethod gMessageQueueMethods[] = {
213 /* name, signature, funcPtr */
214 { "nativeInit", "()J", (void*)android_os_MessageQueue_nativeInit },
215 { "nativeDestroy", "(J)V", (void*)android_os_MessageQueue_nativeDestroy },
216 { "nativePollOnce", "(JI)V", (void*)android_os_MessageQueue_nativePollOnce },
217 { "nativeWake", "(J)V", (void*)android_os_MessageQueue_nativeWake },
218 { "nativeIsPolling", "(J)Z", (void*)android_os_MessageQueue_nativeIsPolling },
219 { "nativeSetFileDescriptorEvents", "(JII)V",
220 (void*)android_os_MessageQueue_nativeSetFileDescriptorEvents },
221 };
这里我们可以发现熟悉的JNI函数定义方式。
1.5.2 如何查找native方法
既然这些关键的类和方法在虚拟机创建后就进行注册了,那我们使用Java类时如何找到对应的JNI方法呢?
这里我们以Android消息机制MessageQueue.java为例,在该类中有如下native方法:
/frameworks/base/core/java/android/os/MessageQueue.java
private native void nativePollOnce(long ptr, int timeoutMillis)
同时我们会发现该类并没有像之前动态注册一样调用System.Loadlibrary,因为这些native方法已经注册过了。
那如何找到JNI方法实现呢,有个最简单的方法就是看类名,这里的类名是android.os.MessageQueue,那么对应的JNI文件一般就是android_os_MessageQueue.cpp,而这些系统注册的JNI文件位置一般是在/framework/base/core/jni,我们找到该目录:
会发现这些文件的名字都有很强的规律。
但是总有意外,有些文件却打破了这个命名规律,比如我们熟悉的Binder,它在Java层全名是android.os.Binder,按理说在JNI中文件名是android_os_binder.cpp,但是它在JNI中的文件名却是android_util_Binder.cpp,至于为什么有少数文件打破这个规律,我也无从得知。
1.6 数据类型转换
由于JNI代码是C++代码,所以Java的方法想和JNI层的方法做到一对一对应,那么Java方法中的参数类型就需要在JNI中也要有对应,那为什么不用C++的数据类型呢?原因非常简单,这俩个语言的数据类型是不通用的。
Java的数据类型分为基本数据类型和引用数据类型,JNI层对这俩种类型也做了区分,下面我们来挨个看看。
1.6.1 基本数据类型的转换
基本数据类型的转换比较简单,除了void外,其他都是在原Java类型基础上加个"j"即可,下表就是基本数据类型对应的JNI类型和签名:
Java | JNI | 签名 |
---|---|---|
byte | jbyte | B |
char | jchar | C |
double | jdouble | D |
float | jfloat | F |
int | jinit | I |
short | jshort | S |
long | jlong | J |
boolean | jboolean | Z |
void | void | V |
1.6.2 引用数据类型转换
引用类型主要包括数组和类,在JNI层中对于数组要以"Array"结尾,对于普通的类,以jobject,对于Class、String和Throwable要做特殊处理。而签名而言,对于非数组类型统一都是"L+classname+;"的格式,具体如下:
Java | JNI | 签名 |
---|---|---|
类Object | jobject | L+class+; |
Class | jclass | Ljava/lang/Class; |
Throwable | jthrowable | Ljava/lang/Throwable; |
String | jstring | Ljava/lang/String; |
Object[] | jobjectArray | [L+classname+; |
byte[] | jbyteArray | [B |
char[] | jcharArray | [C |
double[] | jdoubleArray | [D |
float[] | jfloatArray | [F |
int[] | jintArray | [I |
short[] | jshortArray | [S |
long[] | jlongArray | [J |
boolean[] | jbooleanArray | [Z |
其实上面的JNI类型都不难理解,稍微需要记住的就是Class、Throwable和String这3种特殊的非数组引用类型,其他一律是jobject,我们来看个例子,这是Java中定义的一个native方法:
private native void native_setup(Object mediarecorderThis,
String clientName, @NonNull Parcel attributionSource)
可以发现这里的参数有Object类型,String类型和Parcl类型,找到对应的JNI层实现函数:
android_media_MediaRecorder_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,
jstring packageName, jobject jAttributionSource)
可以发现从Java方法到JNI方法的转变,首先是多了一个类型为JNIEnv指针类型的参数,然后Java中的Object变成了jobject,Java中的String变成了jstring,java中的Parcel变成了jobject,至于为什么多个JNIEnv类型指针,后面我们细说,简单来说就是它代表着Java世界的环境指针,通过该指针可以操控Java代码。
1.7 方法签名
熟悉Java的开发者都知道,Java中可以定义同名但是返回值或者参数不同的方法,这个就是Java的方法重载。在前面定义Java方法和JNI方法关系的结构体中,第一个成员是Java方法名,第二个成员是签名,也只有这俩者结合,才可以确定唯一的Java方法。
所以何为方法签名,就是把方法的参数和返回值类型组合在一起就是方法的签名,格式为:
(参数类型)返回值类型
同时这里的类型也不是Java类型,而是类型的签名,可以看成是一种Java类型的简写,比如在前面我们都罗列过了,Java的int类型对应于I,这些类型签名标识符是运用在Java虚拟机的。
所以现在我们再来看看前面动态注册的方法:
{"native_setup",
"(Ljava/lang/Object;Ljava/lang/String;Landroid/os/Parcel;)V",
(void *)android_media_MediaRecorder_native_setup}
这里我们发现在Java中的方法名为native_setup,签名转换为Java语言就是该方法的返回值是Void,参数分别是Ojbect、String和Parcel,对应的C++方法是一个函数指针。
方法签名不难理解,但是其签名格式真的让人难以记住,直接写容易出错,所以可以使用javap -s -p命令查看一个Java类的所有方法和成员签名,测试代码和效果如下:
PS D:\AllAndroidProject\wayealProject\GuanWangPaiShui\app\src\main\java\com\wayeal\module> javac .\MediaRecorder.java
Picked up _JAVA_OPTIONS: -Xmx1024M
PS D:\AllAndroidProject\wayealProject\GuanWangPaiShui\app\src\main\java\com\wayeal\module> javap -s -p .\MediaRecorder.class
Picked up _JAVA_OPTIONS: -Xmx1024M
Compiled from "MediaRecorder.java"
public class com.wayeal.module.MediaRecorder {
public com.wayeal.module.MediaRecorder();
descriptor: ()V
private static final native void native_init();
descriptor: ()V
private static final native void native_start(java.lang.String, java.io.File, java.lang.Byte[]);
descriptor: (Ljava/lang/String;Ljava/io/File;[Ljava/lang/Byte;)V
static {};
descriptor: ()V
}
有了这个指令生成方法签名,就再也不怕在动态注册时写错方法签名了。
1.8 解析JNIEnv
在前面分析中我们知道Java中的方法对应到JNI中的方法会在参数上多一个JNIEnv指针参数,该参数非常重要,我们来回顾一下前面native_init方法在JNI层的实现:
/frameworks/base/media/jni/android_media_MediaRecorder.cpp
static void
439 android_media_MediaRecorder_native_init(JNIEnv *env)
440 {
441 jclass clazz;
442
443 clazz = env->FindClass("android/media/MediaRecorder");
444 if (clazz == NULL) {
445 return;
446 }
447
448 fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
449 if (fields.context == NULL) {
450 return;
451 }
452
453 fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");
454 if (fields.surface == NULL) {
455 return;
456 }
457
458 jclass surface = env->FindClass("android/view/Surface");
459 if (surface == NULL) {
460 return;
461 }
462
463 fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
464 "(Ljava/lang/Object;IIILjava/lang/Object;)V");
465 if (fields.post_event == NULL) {
466 return;
467 }
468 }
在这个方法中,我们发现env不仅可以获取Java世界的类,还可以调用Java世界的方法,所以这个JNIEnv其实就是Native世界中Java环境的代表,通过JNIEnv* 指针就可以在Native世界中访问Java世界的代码进行操作,它只在创建它的线程中有效,不能跨线程传递,不同线程的JNIEnv是彼此独立的。
为什么有这个JNIEnv也非常好理解,既然JNI作为Java世界和Native世界的"中间人",它本身就是C++代码,所以调用Native的代码是天生的,但是调用Java世界的代码就费劲了,所以这里就需要有个Java世界的代表:JNIEnv。
其实我们可以猜想一下这个JNIEnv的功能能访问Java世界的类和方法,其实现细节肯定也是和虚拟机和AndroidRuntime有关,我们这里不做深入探究,来看一下JNIEnv这个结构体的定义:
/libnativehelper/include/nativehelper/jni.h
struct _JNIEnv {
//C语言
493 const struct JNINativeInterface* functions;
494
//C++
495 #if defined(__cplusplus)
496
497 ...
503
504 jclass FindClass(const char* name)
505 { return functions->FindClass(this, name); }
506
507 }
...
554
555 void DeleteLocalRef(jobject localRef)
556 { functions->DeleteLocalRef(this, localRef); }
557
558 ...
1031 #endif /*__cplusplus*/
1032 };
这里有点离谱,会发现C++时,会多出几百行的结构体定义,至于为啥这么多,其实也很容易想到。因为C++和Java语言差距巨大,这里必须能涉及到所有Java相关的特性,比如类、变量、父类、异常、引用类型、数组、释放内存等等,所以会有方法,这部分就不细说了,需要时可以查看该结构体。
不过这里我们发现结构体中即使是C++代码,也是调用JNINativeInterface* 指针的方法,所以重点就是该数据结构:
/libnativehelper/include/nativehelper/jni.h
struct JNINativeInterface {
...
160 jclass (*FindClass)(JNIEnv*, const char*);
161
162 jmethodID (*FromReflectedMethod)(JNIEnv*, jobject);
...
483 };
在该结构体中,毫不意外地定义了几百个函数指针,这些函数指针都由虚拟机和系统实现,这里我们就不过多研究了,这里只需要有个大概概念,就是JNI还是非常强大的,为了能顺利调用Java世界中的内容,做出了巨大努力。
这里我们来看俩个常用的类型,分别是jmethodID和jfieldID,分别表示Java类中的成员变量和方法,我们再来看看native_init函数,看看能不能读懂代码了:
/frameworks/base/media/jni/android_media_MediaRecorder.cpp
static void
439 android_media_MediaRecorder_native_init(JNIEnv *env)
440 {
//clazz是Java类,类型jclass就是Java中的Class
441 jclass clazz;
//通过env的FindClass找到"android.media.MediaRecoder"这个Java类
443 clazz = env->FindClass("android/media/MediaRecorder");
444 if (clazz == NULL) {
445 return;
446 }
//通过env的GetFieldId,从clazz找到变量名为"mNativeContext"的变量,然后赋值给field对象的context变量
448 fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
449 if (fields.context == NULL) {
450 return;
451 }
//从clazz找到名为"mSurface"的变量,保存到field中
453 fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");
454 if (fields.surface == NULL) {
455 return;
456 }
//找到"android.view.Surface"类
458 jclass surface = env->FindClass("android/view/Surface");
459 if (surface == NULL) {
460 return;
461 }
//找到clazz中名为"postEventFromNative"的静态方法,同时保存到field中
463 fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
464 "(Ljava/lang/Object;IIILjava/lang/Object;)V");
465 if (fields.post_event == NULL) {
466 return;
467 }
468 }
这里代码看似非常费劲,就为了获取MediaRecoder.java类中的俩个变量和一个方法,但是这里面涉及了一个关键知识点就是Class类。
关于Class类,把一个Java代码加载到虚拟机中就会生成一个Class对象,该Class对象描述了Java类的信息,比如有哪些变量、方法等。就比如上面代码中的clazz对象就是Java中的Class对象,通过clazz获取其中的变量和方法就比较方便。
既然这里只是获取了变量和方法,其实和具体对象是没关系的,我们来看看什么时候调用这些,通过阅读代码,我们会发现在notify方法中会调用之前保存这些变量和方法的field变量:
void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
105 {
106 ALOGV("JNIMediaRecorderListener::notify");
107
108 JNIEnv *env = AndroidRuntime::getJNIEnv();
109 env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);
110 }
这里的mClass是通过NewGlobalRef创建的对象,而第二个参数就是前面获取的"postEventFromNative"方法,所以这里会调用Java中的postEventFromNative方法:
private static void postEventFromNative(Object mediarecorder_ref,
1150 int what, int arg1, int arg2, Object obj)
1151 {
1152 MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get();
1153 if (mr == null) {
1154 return;
1155 }
1156
1157 if (mr.mEventHandler != null) {
1158 Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);
1159 mr.mEventHandler.sendMessage(m);
1160 }
1161 }
这样我们就可以完整地看见JNI是如何调用Java层代码了。
1.9 引用类型
熟悉Java的同学都知道编写Java代码是不用处理其内存回收的,因为Java虚拟机的GC机制即垃圾回收机制会帮我们回收那些不用的对象的内存。那JNI作为Java和C/C++的中间人,既可以调用Java代码又可以调用C++代码,那该如何做内存回收呢?
和Java的引用类型一样,JNI也有自己的引用类型,它们分别是本地引用(Local References)、全局引用(Global References)和弱全局引用(Weak Global References)。从名字就可以看出它们大概对应Java中的局部变量,类静态变量和弱引用,有着不同的生命周期和GC机制,我们就来分别介绍。
1.9.1 本地引用
JNIEnv提供的函数所返回的引用基本上都是本地引用,所以本地引用是JNI中最常见的引用类型,特点如下:
- 当Native函数返回时,这个本地引用就会被自动释放。
- 只在创建它的线程中有效,不能跨线程使用。
- 局部引用是JVM负责的引用类型,受JVM管理。
这里举个熟悉的例子:
/frameworks/base/media/jni/android_media_MediaRecorder.cpp
static void
439 android_media_MediaRecorder_native_init(JNIEnv *env)
440 {
441 jclass clazz;
442
443 clazz = env->FindClass("android/media/MediaRecorder");
444 if (clazz == NULL) {
445 return;
446 }
...
这里clazz的类型是jclass,是引用数据类型,就是本地引用,也就是方法内的局部变量。它会在native_init()方法执行完会自动释放,我们也可以使用JNIEnv的DeleteLocalRef函数来手动删除本地引用,该函数使用场景是在native函数返回前占用了大量内存,需要调用DeleteLocalRef函数立即删除本地引用。
1.9.2 全局引用
通过JNIEnv的NewGlobalRef函数可以创建全局引用,该种引用和本地引用几乎相反,有如下特点:
- 在native函数返回时不会被自动释放,因此全局引用需要手动来进行释放,并且不会被GC回收。
- 全局引用是可以跨线程使用的。
- 全局引用不受JVM管理。
这里举个例子,JNIMediaRecorderListener是JNI层中的一个C++类,构造函数如下:
JNIMediaRecorderListener::JNIMediaRecorderListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
79 {
83 jclass clazz = env->GetObjectClass(thiz);
84 if (clazz == NULL) {
85 ALOGE("Can't find android/media/MediaRecorder");
86 jniThrowException(env, "java/lang/Exception", NULL);
87 return;
88 }
//这里创建了俩个全局引用变量
89 mClass = (jclass)env->NewGlobalRef(clazz);
93 mObject = env->NewGlobalRef(weak_thiz);
94 }
然后在其析构函数中需要对这俩个变量进行释放:
JNIMediaRecorderListener::~JNIMediaRecorderListener()
97 {
99 JNIEnv *env = AndroidRuntime::getJNIEnv();
100 env->DeleteGlobalRef(mObject);
101 env->DeleteGlobalRef(mClass);
102 }
所以使用全局引用时,要多加小心。
1.9.3 弱全局引用
这是一个特殊的全局引用,它和全局引用的特点相识,不同的是弱全局是可以被GC回收的,弱全局引用被GC回收后,会指向NULL。
说实话,我有点不太清楚这种弱全局引用的作用,因为它有个致命问题,就是使用NewWeakGlobalRef创建的弱全局引用变量,在使用时还需要调用IsSameObject来判断有没有被GC回收,实在是增加了很多工作量。
2. 总结
JNI技术作为Java世界和Native世界的桥梁,起着非常重要的作用,它是由虚拟机和运行时环境共同实现。现在做个小节:
- 首先需要理解动态库so是什么,以MediaRecorder.java为例,要能读懂这种Java和C++互相调用的代码。
- 对于JNI注册的方式,静态注册就是利用javap命令生成头文件,以方法名构建联系;而动态注册则是手动进行Java方法和native方法关联,通过会必须被调用的JNI_OnLoad函数完成注册。
- 系统在创建完虚拟机后就会注册一大堆JNI函数,通过类名,我们可以找到常见类的JNI函数实现位置。
- 对于特殊的JNI数据类型,只需要理解Java数据类型,不难理解JNI的数据类型。
- 方法签名是用来确定唯一Java方法的,就是参数和返回值的结合体。
- JNIEnv非常关键,它是native环境中Java世界的代表,通过它可以调用Java世界代码。
- 对于JNI中的对象,同样可以设置不同引用来设置其生命周期和释放方式。
JNI技术至关重要,后面分析虚拟机时,会再进行探究。
转载自:https://juejin.cn/post/7139843955457261605