目录
JNI概述
MediaRecorder框架中的JNI
Java Framework层的MediaRecorder
JNI层的MediaRecorder
Native方法注册
数据类型的转换
方法签名
解析JNIEnv
参考《Android进阶解密》
JNI概述
JNI(Java Native Interface,Java本地接口),是Java与其他语言通信的桥梁。这不是Android系统所独有的,而是Java所有,当出现一些用Java语言无法处理的任务时,就可以使用JNI技术来实现。
JNI不只是应用于Android开发,它有着非常广泛的应用场景。JNI在Android中的应用主要有:音视频开发、热修复、插件话、逆向开发、系统源码调用等等。为了方便使用JNI技术,Android提供了NDK这个工具集合,NDK开发是基于JNI的,它和JNI开发本质上并没有区别,理解JNI原理,NDK开发也会很容易掌握。
Android系统按语言来划分的话分为两个层面:分别是Java层和Native层。通过JNI,Java层可以访问Native层,同样的Native层也可以访问Java层。下面以MediaRecorder框架中的JNI举例来理解系统中的JNI
MediaRecorder框架中的JNI
MediaRecorder是Android系统提供给我们用于录音和录像的框架。Java Framework层对应的是MediaRecorder.java
,也就是我们平时开发在应用中直接调用的类。JNI层对应的是libmedia_jni.so
,它是JNI的一个动态库。Native层对应的是libmedia.so
,这个动态库完成来实际的功能。
Java Framework层的MediaRecorder
我们先看一下MediaRecorder.java
的源码:
frameworks/base/media/java/android/media/MediaRecorder.java
public class MediaRecorder{
static {
//加载名字为media_jni的动态库
System.loadLibrary("media_jni");
native_init();
}
......
//JNI注册
private static native final void native_init();
......
public native void start() throws IllegalStateException;
......
}
上述代码指截取部分JNI相关的代码:
- 在静态代码块中首先加载名字为
media_jni
的动态库,也就是libmedia_jni.so
。 - 然后接着调用了
native_init ()
方法,该方法会调用Native层方法,用来完成JNI的注册。 -
start()
方法也是一个Native方法。
对于Java Framework层来说只需要加载对应的JNI库,接着声明native方法就可以了,剩下的工作由JNI层来完成。
JNI层的MediaRecorder
MediaRecorder的JNI层是由android_media_MediaRecorder.cpp
实现,native方法:native_init
和start
的代码实现如下:
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.");
}
android_media_MediaRecorder_native_init方法
是native_init方法
在JNI层的实现;android_media_MediaRecorder_start方法
是start方法
在JNI层的实现。那么它们是如何找到对应的方法的呢?下面我们首先了解一下JNI方法注册的知识。
Native方法注册
Native方法注册分为动态注册和静态注册,其中静态注册多用于NDK开发,而动态注册多用于Framework开发。下面分别来看一下这两种注册方式。
静态注册
在Android Studio中新建一个Java Library,命名为media,仿照系统的MediaRecorder.java,代码如下:
public class MediaRecorder {
static{
System.loadLibrary("media_jni");
native_init();
}
private static native final void native_init();
public native void start() throws IllegalStateException;
}
编写完成后,对MediaRecorder.java
进行编译和生成JNI方法:进入项目的media/src/main/java
目录中,执行以下命令:
javac com/example/media/MediaRecorder.java //编译
javah com.example.media.MediaRecorder //生成头文件
第二个命令会生成com_example_media_MediaRecorder.h
文件,内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_media_MediaRecorder */
#ifndef _Included_com_example_media_MediaRecorder
#define _Included_com_example_media_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_media_MediaRecorder
* Method: native_init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_media_MediaRecorder_native_1init
(JNIEnv *, jclass);
/*
* Class: com_example_media_MediaRecorder
* Method: start
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_media_MediaRecorder_start
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
在Java中的native_init()
方法被声明为Java_com_example_media_MediaRecorder_native_1init
方法,以“Java”开头说明是在Java平台中调用JNI方法的,后面的com_example_media_MediaRecorder_native_1init
指的是包名 + 类名 + 方法名的格式。我们会发现还多了一个1
,这是因为Java中的native_init
方法包含了"_",转换成JNI方法后变成了“_1”。
此外方法还多了几个参数:
- JNIEnv:是Native层中Java环境的代表,通过该类型的指针就可以在Native层中访问Java层的代码,它只在创建它的线程中有效,不能跨线程传递。
-
jclass:是JNI的属性类型,对应Java的
java.lang.Class
实例。 - jobject:同样也是JNI属性类型,对应Java的Object。
当我们在Java中调用native_init()
方法时,就会从JNI中寻找Java_com_example_media_MediaRecorder_native_1init
函数,如果没有就会报错,如果有就会为native_init
和Java_com_example_media_MediaRecorder_native_1init
建立关联,其实就是报错JNI的函数指针。这样再次调用的时候直接使用这个函数指针就可以了。
静态注册就是根据方法名,将Java方法和JNI函数建立关联,这样会有一些缺点:
- JNI层函数名过长。
- 声明native方法的类需要用javah生成头文件。
- 初次调用native方法时需要建立关联,影响效率。
动态注册
JNI中有一种结构用来记录Java的native方法和JNI方法的关联关系,它就是JNINativeMethod,它在jni.h
中被定义:
typedef struct {
const char* name;//Java方法名字
const char* signature;//Java方法的签名
void* fnPtr;//JNI中对应方法的指针
} JNINativeMethod;
系统的MediaRecorder采用的是动态注册,下面看一下它的JNI层是怎么做的:
frameworks/base/media/jni/android_media_MediaRecorder.cpp
//JNINativeMethod类型的数组,数组名字为gMethods
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},
......
};
上面定义了一个JNINativeMethod类型的数组,数组的名字是gMethods
,里面存储的是native方法于JNI层函数的对应关系。只定义是没有用的,还需要注册它,注册的函数为:register_android_media_MediaRecorder
:
// 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));
}
通过该方法的注释我们知道该方法是在JNI_OnLoad
函数中调用的。这个函数会在System.loadLibrary
函数后调用,因为多媒体框架中很多框架都要进行JNINativeMethod类型的数组注册,因此函数注册被统一定义在android_media_MediaPlayer.cpp
的JNI_OnLoad
函数中,该函数的代码如下:
frameworks/base/media/jni/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);
......
if (register_android_media_MediaPlayer(env) < 0) {
ALOGE("ERROR: MediaPlayer native registration failed\n");
goto bail;
}
if (register_android_media_MediaRecorder(env) < 0) {
ALOGE("ERROR: MediaRecorder native registration failed\n");
goto bail;
}
......
/* success -- return valid version number */
result = JNI_VERSION_1_4;
bail:
return result;
}
在register_android_media_MediaRecorder
方法中返回了AndroidRuntime::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);
}
在该方法中又返回了jniRegisterNativeMethods
函数,该函数被定义在JNI的帮助类JNIHelp.cpp中:
libnativehelper/JNIHelp.cpp
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
......
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
char* tmp;
const char* msg;
if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
// Allocation failed, print default warning.
msg = "RegisterNatives failed; aborting...";
} else {
msg = tmp;
}
e->FatalError(msg);
}
return 0;
}
从上面代码可以看出最终调用的是JNIEnv的RegisterNatives
函数来完成JNI注册的。
动态注册要比静态注册复杂一些。但是它解决来静态注册的缺点,可以说一劳永逸。
动态注册是直接存储Java的native方法与它对应的JNI中的函数指针。
数据类型的转换
Java层的数据类型到来JNI层就需要转换为JNI层的数据类型。Java的数据类型分为基本数据类型和引用数据类型,JNI层对于这两种数据类型也做来区分,下面就分别来看一下。
基本数据类型的转换
Java | Native | Signature |
---|---|---|
byte | jbyte | B |
char | jchar | C |
double | jdouble | D |
float | jfloat | F |
int | jint | I |
short | jshort | S |
long | jlong | J |
boolean | jboolean | Z |
void | void | V |
除了最后一个void,其他的数据类型只需要在前面加上“j”就可以了。Signature表示的是签名格式。
引用数据类型转换
Java | Native | Signature |
---|---|---|
Object | jobject | L + classname + ; |
Class | jclass | Ljava/lang/Class; |
String | jstring | Ljava/lang/String; |
Throwable | jthrowable | Ljava/lang/Throwable; |
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 |
下面以MediaRecorder为例看一下类型的转换:
frameworks/base/media/java/android/media/MediaRecorder.java
private native void _setOutputFile(FileDescriptor fd, long offset, long length)
throws IllegalStateException, IOException;
_setOutputFile
方法对应的JNI层的方法为:
frameworks/base/media/jni/android_media_MediaRecorder.cpp
static void
android_media_MediaRecorder_setOutputFileFD(JNIEnv *env, jobject thiz, jobject fileDescriptor, jlong offset, jlong length)
{
......
}
对比以上两个方法可以看到** FileDescriptor被转换成了 jobject, long被转换成了 jlong**。
方法签名
方法签名是由签名格式组成的,上面在介绍数据类型转换的时候每种数据类型都给出了对应的签名格式。那么方法签名有什么用呢?我们先看一下方法签名是什么样子的:
``
static const JNINativeMethod gMethods[] = {
......
{"native_init", "()V", (void *)android_media_MediaRecorder_native_init},
{"native_setup", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V",
......
};
其中“()V”和"(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"就是方法签名。Java中的方法是有重载的,可以定义同名的方法,但参数不同。正因为如此,在JNI中通过方法名是无法找到Java中对应的具体方法的,JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。
JNI方法签名的格式为:
(参数1签名格式参数2签名格式...)返回值签名格式
以native_setup
函数为例,它在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
native_setup函数的第一个参数的签名为:"Ljava/lang/Object;",第2和第3个参数的签名为:“Ljava/lang/String;”,返回值的签名格式为:“V”。通过参数的签名格式我们可以找到java中对应的参数类型。
通过Java提供的javap
命令可以自动生成方法签名。
解析JNIEnv
JNIEnv是Native层中Java环境的代表,通过JNIEnv *
指针就可以在Native层中访问Java层中的代码。它只在创建它的线程中有效,不能跨线程传递,因此不同线程的JNIEnv是彼此独立的。
JNIEnv的主要作用:
- 调用Java的方法。
- 操作Java中的变量和对象等。
JNIEnv的定义如下:
libnativehelper/include/nativehelper/jni.h
#if defined(__cplusplus)
//C++中JNIEnv类型
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
//C语言中JNIEnv类型
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
这里使用预定义宏__cplusplus
来区分C语言和C++两种代码,如果定义了__cplusplus
(编译的是C++源文件),就使用C++代码中的定义,否则就是C语言的定义。JavaVM
:它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都共享这个JavaVM。通过JavaVM的AttachCurrentThread
函数可以获取这个线程的JNIEnv
,这样就可以在不同的线程中调用Java方法了。
在使用
AttachCurrentThread
函数的线程退出前,务必要调用DetachCurrentThread
函数来释放资源。
在C++中JNIEnv
的类型是_JNIEnv,下面我们看一下它是如何定义的:
libnativehelper/include/nativehelper/jni.h
struct _JNIEnv {
const struct JNINativeInterface* functions;
#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); }
......
}
从上面代码可以看到_JNIEnv是一个结构体,其中内部又包含了JNINativeInterface。在_JNIEnv中定义了很多的函数。这里只贴出了比较常用的3个函数,FindClass
函数用来找到Java中指定名称的类,GetMethodID
函数用来获取Java中的方法,GetFieldID
函数用来获取Java中的成员变量。这3个函数都调用了JNINativeInterface中定义的函数,因此可以看出,无论是C语言还是C++,JNIEnv的类型都和JNINativeInterface有关系,下面看一下它的定义:
libnativehelper/include/nativehelper/jni.h
struct JNINativeInterface {
......
jclass (*FindClass)(JNIEnv*, const char*);
......
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
......
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
......
}
JNINativeInterface同样也是一个结构体,在它里面定义了很多和JNIEnv结构体对应的函数指针。通过这些函数指针的定义,就能够定义到虚拟机中的JNI函数表,从而实现了JNI层在虚拟机中的函数调用,这样JNI就可以调用Java层的方法了。
在C语法中,
JNIEnv
是一个结构体指针:struct JNINativeInterface* JNIEnv
。
jfieldID和jmethodID
在_JNIEnv结构体中定义了很多的函数,这些函数都会有不同的返回值,如下所示:
struct _JNIEnv {
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
......
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); }
......
}
这个两个函数的返回值分别为:jmethodID
和jfieldID
,分别用来代表Java类中的方法和成员变量。jclass
代表Java类,name
:代表方法名或者成员变量的名字,sig
:代表这个方法或者成员变量的签名。接下来我们看一下这两个函数在MediaRecorder框架中的使用:
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.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;
}
}
在上述函数的开始处,通过FindClass
函数来找到Java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz
,所以,clazz
就是Java层MediaRecorder在JNI层的代表。紧接着找到Java层的MediaRecorder中名字为mNativeContext
和mSurface
的成员变量,并分别赋值给fields
的context
,surface
,最后找到名字为postEventFromNative
的静态方法,并赋值给fields
的post_event
。其中fields
的定义如下:
frameworks/base/media/jni/android_media_MediaRecorder.cpp
struct fields_t {
jfieldID context;
jfieldID surface;
jmethodID post_event;
};
static fields_t fields;
在android_media_MediaRecorder_native_init函数中
将Java层中的成员变量和方法赋值给了jfieldID
和jmethodID
保存起来,这样不用每次调用的时候都去查询。下面看一下它们是如何使用的:
frameworks/base/media/jni/android_media_MediaRecorder.cpp
void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
{
ALOGV("JNIMediaRecorderListener::notify");
JNIEnv *env = AndroidRuntime::getJNIEnv();
env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);
}
调用CallStaticVoidMethod
函数时传入的参数就包含了fields.post_event
,该参数代表的是Java层MediaRecorder的静态方法postEventFromNative
,下面看一下该方法的实现:
frameworks/base/media/java/android/media/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) {
Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);
mr.mEventHandler.sendMessage(m);
}
}
在该方法中创建了一个消息,然后通过mEventHandler来发送处理,这样就会切换到应用程序的主线程中。该方法是通过JNIEnv的CallStaticVoidMethod
函数来调用的,也就是说通过它可以访问Java层的静态方法,同理,通过CallVoidMethod
函数可以访问Java层的非静态方法。
引用类型
和Java的引用类型一样,JNI也有引用类型,它们分别是本地引用(Local References)、全局引用(Global References)和弱全局引用(Weak Global References)。
本地引用
JNIEnv提供的函数所返回的引用基本上都是本地引用,因此本地引用也是JNI中最常见的引用类型。本地引用的特点主要有以下几点:
- 当native函数返回时,这个本地引用就会被自动释放。
- 只在创建它的线程有效,不能跨线程使用。
- 局部引用是JVM负责的引用类型,受JVM管理。
下面通过一个示例来说明:
frameworks/base/media/jni/android_media_MediaRecorder.cpp
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
return;
}
......
}
FindClass
函数返回的clazz
就是本地引用,它会在android_media_MediaRecorder_native_init
函数调用返回后自动释放,我们也可以调用JNIEnv的DeleteLocalRef
函数来手动删除本地引用,该函数的应用场景主要是在native函数返回之前占用了大量内存,需要手动删除本地引用。
全局引用
全局引用和本地引用几乎是相反的,它主要有以下几个特点:
- 在native函数返回时不会被自动释放,因此全局引用需要手动进行释放,并且不会被GC回收。
- 全局引用是可以跨线程使用的。
- 全局引用不受JVM管理。
JNIEnv的NewGlobalRef
函数用来创建全局引用,调用DeleteLocalRef
函数来释放全局引用。
下面通过一个示例来看一下全局引用的使用:
``
JNIMediaRecorderListener::JNIMediaRecorderListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
{
jclass clazz = env->GetObjectClass(thiz);
if (clazz == NULL) {
ALOGE("Can't find android/media/MediaRecorder");
jniThrowException(env, "java/lang/Exception", NULL);
return;
}
mClass = (jclass)env->NewGlobalRef(clazz);
mObject = env->NewGlobalRef(weak_thiz);
}
clazz
是本地引用,在下面通过NewGlobalRef
函数将它变成了全局引用mClass
,该全局引用是在JNIMediaRecorderListener的析构函数中释放,这里就不贴出源码了。
弱全局引用
弱全局引用是一种特殊的全局引用,它和全局引用的特点相似,不同的是弱全局引用是可以被GC回收的,被回收后会指向NULL
。通过JNI的NewWeakGlobalRef
函数来创建弱全局引用,调用DeleteWeakGlobalRef
函数来释放弱全局引用,由于它可能被GC回收,因此在使用之前要先判断它是否被回收了,通过IsSameObject
函数来判断。
网友评论