Android深入理解JNI

前言

JNI不仅仅在NDK开发中应用,它更是Android系统中Java与Native交互的桥梁,不理解JNI的话,你就只能停留在Java Framework层。这一个系列我们来一起深入学习JNI。

JNI概述

Android系统按语言来划分的话由两个世界组成,分别是Java世界和Native世界。那为什么要这么划分呢?Android系统由Java写不好吗?除了性能的之外,最主要的原因就是在Java诞生之前,就有很多程序和库都是由Native语言写的,因此,重复利用这些Native语言编写的库是十分必要的,况且Native语言编写的库具有更好的性能。
这样就产生了一个问题,Java世界的代码要怎么使用Native世界的代码呢,这就需要一个桥梁来将它们连接在一起,而JNI就是这个桥梁。
未命名文件(5).png
通过JNI,Java世界的代码就可以访问Native世界的代码,同样的,Native世界的代码也可以访问Java世界的代码。
为了讲解JNI我们需要分析系统的源码,在即将出版的《Android进阶之光》的最后一章中我拿MediaPlayer框架做了举例,这里换MediaRecorder框架来举例,它和MediaPlayer框架的调用过程十分类似。

MediaRecorder框架概述

MediaRecorder我们应该都不陌生,它用于录音和录像。这里不会主要介绍MediaRecorder框架,而是MediaRecorder框架中的JNI。
未命名文件(6).png
Java世界对应的是MediaRecorder.java,也就是我们应用开发中直接调用的类。JNI层对用的是libmedia_jni.so,它是一个JNI的动态库。Native层对应的是libmedia.so,这个动态库完成了实际的调用的功能。

Java层的MediaRecorder

我们先来查看MediaRecorder.java的源码,截取部分和JNI有关的部分如下所示。
frameworks/base/media/java/android/media/MediaRecorder.java

1
2
3
4
5
6
7
8
9
10
11
public class MediaRecorder{
static {
System.loadLibrary("media_jni");//1
native_init();//2
}
...
private static native final void native_init();//3
...
public native void start() throws IllegalStateException;
...
}

在静态代码块中首先调用了注释1处的代码,用来加载名为“media_jni“的动态库,也就是libmedia_jni.so。接着调用注释2处的native_init方法,注释3处的native_init方法用native来修饰,说明它是一个native方法,表示由JNI来实现。MediaRecorder的start方法同样也是一个native方法。
对于Java层来说只需要加载对应的JNI库,接着声明native方法就可以了,剩下的工作由JNI层来完成。

JNI层的MediaRecorder

MediaRecorder的JNI层由android_media_recorder.cpp实现,native方法native_init和start的JNI层实现如下所示。
frameworks/base/media/jni/android_media_MediaRecorder.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
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层的实现。那么,native_init方法是如何找到对应的android_media_MediaRecorder_native_init方法的呢?
这就需要了解JNI方法注册的知识。

JNI方法注册

JNI方法注册分为静态注册和动态注册,其中静态注册多用于NDK开发,而动态注册多用于Framework开发。

静态注册

在AS中新建一个Java Library名为media,这里仿照系统的MediaRecorder.java,写一个简单的MediaRecorder.java,如下所示。

1
2
3
4
5
6
7
8
9
10
package com.example;
public class MediaRecorder {
static {
System.loadLibrary("media_jni");
native_init();
}

private static native final void native_init();
public native void start() throws IllegalStateException;
}

接着进入项目的media/src/main/java目录中执行如下命令:

1
2
javac com/example/MediaRecorder.java
javah com.example.MediaRecorder

第二个命令会在当前目录中(media/src/main/java)生成com_example_MediaRecorder.h文件,如下所示。

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
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_MediaRecorder */

#ifndef _Included_com_example_MediaRecorder
#define _Included_com_example_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_MediaRecorder
* Method: native_init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_native_1init
(JNIEnv *, jclass);//1

/*
* Class: com_example_MediaRecorder
* Method: start
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_start
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

nativeinit方法被声明为注释1处的方法,也就是Java_com_example_MediaRecorder_native1init,”Java“ 开头说明是在Java平台中调用JNI方法,后面的com_example_MediaRecorder_native1init指的是包名+类名+方法名的格式,原本在Java中应该是以”.”来进行分割,这里却用了”“,这是因为在Native语言中”.”有特殊的含义。还有眼尖的同学发现了注释1处的方法名还多了一个“1”,这是因为Java的nativeinit方法包含了”“,转换成JNI方法后会变成”_1”。

其中JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递。jclass是JNI的数据类型,对应Java的java.lang.Class实例。jobject同样也是JNI的数据类型,对应于Java的Object。关于JNIEnv 以及JNI的数据类型会在本系列的后续文章中进行介绍。

当我们在Java中调用native_init方法时,就会从JNI中寻找Java_com_example_MediaRecorder_native_1init方法,如果没有就会报错,如果找到就会为native_init和Java_com_example_MediaRecorder_native_1init建立关联,其实是保存JNI的方法指针,这样再次调用native_init方法时就会直接使用这个方法指针就可以了。
静态注册就是根据方法名,将Java方法和JNI方法建立关联,但是它有一些缺点:

  • JNI层的方法名称过长。
  • 声明Native方法的类需要用javah生成头文件。
  • 初次调用JIN方法时需要建立关联,影响效率。

我们知道,静态注册就是Java的Native方法通过方法指针来与JNI进行关联的,如果Native方法知道它在JNI中对应的方法指针,就可以避免上述的缺点,这就是动态注册。

动态注册

JNI中有一种结构用来记录Java的Native方法和JNI方法的关联关系,它就是JNINativeMethod,它在jni.h中被定义:

1
2
3
4
5
typedef struct {
const char* name;//Java方法的名字
const char* signature;//Java方法的签名信息
void* fnPtr;//JNI中对应的方法指针
} JNINativeMethod;

系统的MediaRecorder采用的就是动态注册,我们来查看它的JNI层是怎么做的。
frameworks/base/media/jni/android_media_MediaRecorder.cpp

1
2
3
4
5
6
7
8
9
10
11
static const JNINativeMethod gMethods[] = {
...
{"start", "()V", (void *)android_media_MediaRecorder_start},//1
{"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数组,里面存储的就是MediaRecorder的Native方法与JNI层方法的对应关系,其中注释1处”start”是Java层的Native方法,它对应的JNI层的方法为android_media_MediaRecorder_start。”()V”是start方法的签名信息,关于Java方法的签名信息后续的文章会介绍。
只定义JNINativeMethod 类型的数组是没有用的,还需要注册它,注册的方法为register_android_media_MediaRecorder:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

1
2
3
4
5
6
//JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaRecorder(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaRecorder", gMethods, NELEM(gMethods));
}

register_android_media_MediaRecorder方法中return了AndroidRuntime的registerNativeMethods方法,如下所示。
frameworks/base/core/jni/AndroidRuntime.cpp

1
2
3
4
5
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

registerNativeMethods方法中又return了jniRegisterNativeMethods方法:
libnativehelper/JNIHelp.cpp

1
2
3
4
5
6
7
8
9
10
11
extern "C" int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
...
if (env->RegisterNatives(c.get(), gMethods, numMethods) < 0) {//1
char* msg;
(void)asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
env->FatalError(msg);
}
return 0;
}

从注释1处可以看出,最终调用的JNIEnv的RegisterNatives方法,JNIEnv在JNI中十分重要,后续文章会介绍它。

register_android_media_MediaRecorder方法最终会调用JNIEnv的RegisterNatives方法,但是register_android_media_MediaRecorder方法是在哪被调用的呢?答案在register_android_media_MediaRecorder方法的注释上:JNI_OnLoad in android_media_MediaPlayer.cpp。这个JNI_OnLoad方法会在System.loadLibrary方法后调用,因为多媒体框架中的很多框架都要进行JNINativeMethod数组注册,因此,注册方法就被统一定义在android_media_MediaPlayer.cpp中的JNI_OnLoad方法中,如下所示。
frameworks/base/media/jni/android_media_MediaPlayer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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) {//1
ALOGE("ERROR: MediaRecorder native registration failed\n");
goto *bail;
}
...
result = JNI_VERSION_1_4;
bail:
return result;
}

在JNI_OnLoad方法中调用了整个多媒体框架的注册JNINativeMethod数组的方法,注释1处的调用了register_android_media_MediaRecorder方法,同样的,MediaPlayer框架的注册JNINativeMethod数组的方法register_android_media_MediaPlayer也被调用了。

原文:http://liuwangshu.cn/framework/jni/1-mediarecorder_register.html

数据类型的转换

android_media_MediaRecorder.cpp中的android_media_MediaRecorder_start方法:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

1
2
3
4
5
6
7
static void
android_media_MediaRecorder_start(JNIEnv *env, jobject thiz)
{
ALOGV("start");
sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
process_media_recorder_call(env, mr->start(), "java/lang/RuntimeException", "start failed.");
}

android_media_MediaRecorder_start方法有一个参数为jobject类型,它是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
所有对象 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

从上表可一看出,数组的JNI层数据类型需要以“Array”结尾,签名格式的开头都会有“[”。除了数组以外,其他的引用数据类型的签名格式都会以“;”结尾。
另外,引用数据类型还具有继承关系,如下所示:

1337955954_3405.jpg

再来列举MediaRecorder框架的Java方法:
frameworks/base/media/java/android/media/MediaRecorder.java

1
2
private native void _setOutputFile(FileDescriptor fd, long offset, long length)
throws IllegalStateException, IOException

_setOutputFile方法对应的JNI层的方法为:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

1
2
3
4
5
static void
android_media_MediaRecorder_setOutputFileFD(JNIEnv *env, jobject thiz, jobject fileDescriptor, jlong offset, jlong length)
{
...
}

对比这两个方法可以看到,FileDescriptor类型转换为了jobject类型 ,long类型转换为了jlong类型。

方法签名

前面表格已经列举了数据类型的签名格式,方法签名就由签名格式组成,那么,方法签名有什么作用呢?我们看下面的代码。
frameworks/base/media/jni/android_media_MediaRecorder.cpp

1
2
3
4
5
6
7
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",
(void *)android_media_MediaRecorder_native_setup},
...
};

gMethods数组中存储的是MediaRecorder的Native方法与JNI层方法的对应关系,
其中”()V”和 “(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V”就是方法签名。我们知道Java是有重载方法的,可以定义方法名相同,但参数不同的方法,正因为如此,在JNI中仅仅通过方法名是无法找到 Java中的具体方法的,JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。
JNI的方法签名的格式为:

1
(参数签名格式...)返回值签名格式

拿上面gMethods数组的native_setup方法举例,他在Java中是如下定义的:

1
2
private native final void native_setup(Object mediarecorder_this,
String clientName, String opPackageName) throws IllegalStateException;

它在JNI中的方法签名为:

1
(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"

参照本文第一节给出的类型转换表格,native_setup方法的第一个参数的签名为“Ljava/lang/Object;”,后两个参数的签名为“Ljava/lang/String;”,返回值类型void 的签名为“V”,组合起来就是上面的方法签名。

如果我们每次编写JNI时都要写方法签名,也会是一件比较头疼的事,幸好Java提供了javap命令来自动生成方法签名。我们先写一个简单的MediaRecorder.java包含上面的native_setup方法:

1
2
3
4
5
6
7
8
9
public class MediaRecorder {
static {
System.loadLibrary("media_jni");
native_init();
}
private static native final void native_init();
private native final void native_setup(Object mediarecorder_this,
String clientName, String opPackageName) throws IllegalStateException;
}

这个文件的在我的本地地址为D:/Android/MediaRecorder.java,接着执行如下命令:

1
javac D:/Android/MediaRecorder.java

执行命令后会生成MediaRecorder.class文件,最后使用javap命令:

1
javap -s -p D:/Android/MediaRecorder.class

其中s 表示输出内部类型签名,p表示打印出所有的方法和成员(默认打印public成员),最终会在cmd中的打印结果如下:

QQ截图20170629223104.pngQQ截图

可以很清晰的看到输出的native_setup方法的签名和此前给出的一致。

JNIEnv

JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,因此,不同线程的JNIEnv是彼此独立的,JNIEnv的主要作用有两点:
1.调用Java的方法。
2.操作Java(获取Java中的变量和对象等等)。

先来看JNIEnv的定义,如下所示。
libnativehelper/include/nativehelper/jni.h

1
2
3
4
5
6
7
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;//C++中JNIEnv的类型
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;//C中JNIEnv的类型
typedef const struct JNIInvokeInterface* JavaVM;
#endif

这里使用预定义宏__cplusplus来区分C和C++两种代码,如果定义了__cplusplus,则是C++代码中的定义,否则就是C代码中的定义。
在这里我们也看到了JavaVM,它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。通过JavaVM的AttachCurrentThread函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。还要记得在使用AttachCurrentThread函数的线程退出前,务必要调用DetachCurrentThread函数来释放资源。

jfieldID和jmethodID

在JNI中用jfieldID和jmethodID来代表Java类中的成员变量和方法,可以通过JNIEnv的下面两个方法来分别得到:

1
2
jfieldID  GetFieldID(jclass clazz,const char *name,const char *sig);
jmethodID GetFieldID(jclass clazz,const char *name,const char *sig);

其中,jclass代表Java类,name代表成员方法或者成员变量的名字,sig为这个方法和变量的签名。
我们来查看MediaRecorder框架的JNI层是如何使用上述的两个方法的,如下所示。
frameworks/base/media/jni/android_media_MediaRecorder.cpp

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
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");//1
if (clazz == NULL) {
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");//2
if (fields.context == NULL) {
return;
}
fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");//3
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");//4
if (fields.post_event == NULL) {
return;
}
}

注释1处,通过FindClass来找到Java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz,因此,clazz就是Java层的MediaRecorder在JNI层的代表。注释2和注释3处的代码用来找到Java层的MediaRecorder中名为mNativeContext和mSurface的成员变量,并分别赋值给context和surface。注释4出获取Java层的MediaRecorder中名为postEventFromNative的静态方法,并赋值给post_event。其中fields的定义为:

1
2
3
4
5
6
struct fields_t {
jfieldID context;
jfieldID surface;
jmethodID post_event;
};
static fields_t fields;

将这些成员变量和方法赋值给jfieldID和jmethodID类型的变量主要是为了效率考虑,如果每次调用相关方法时都要进行查询方法和变量,显然会效率很低,因此在MediaRecorder框架JNI层的初始化方法android_media_MediaRecorder_native_init中将这些jfieldID和jmethodID类型的变量保存起来,以供后续使用。

使用jfieldID和jmethodID

我们保存了jfieldID和jmethodID类型的变量,接着怎么使用它们呢,如下所示。
frameworks/base/media/jni/android_media_MediaRecorder.cpp

1
2
3
4
5
6
7
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);//1
}

在注释1处调用了JNIEnv的CallStaticVoidMethod函数,其中就传入了fields.post_event,从上面我们得知,它其实是保存了Java层MediaRecorder的静态方法postEventFromNative:
frameworks/base/media/java/android/media/MediaRecorder.java

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

这样我们就能在JNI层中访问Java的静态方法了。同理,如果想要访问Java的方法则可以使用JNIEnv的CallVoidMethod函数。
上面的例子是使用了jmethodID,接着来查看jfieldID:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

1
2
3
4
5
6
7
8
9
10
11
static void
android_media_MediaRecorder_prepare(JNIEnv *env, jobject thiz)
{
ALOGV("prepare");
sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
jobject surface = env->GetObjectField(thiz, fields.surface);//1
if (surface != NULL) {
const sp<Surface> native_surface = get_surface(env, surface);
...
}
process_media_recorder_call(env, mr->prepare(), "java/io/IOException", "prepare failed.");

在注释1处调用了JNIEnv的GetObjectField函数,参数中的fields.surface用来保存Java层MediaRecorde中的成员变量mSurface,mSurface的类型为Surface,这样通过GetObjectField函数就得到了mSurface在JNI层中对应的jobject类型变量surface 。

原文:http://liuwangshu.cn/framework/jni/2-signature-jnienv.html