通过MediaScanner理解JNI原理

参考链接:

《深入理解Android 卷I》

什么是JNI

JNI是Java Native Interface的缩写(Java本地调用),Java程序中的函数可以调用Native语言写的函数(一般指的是C/C++编写的函数),Native语言写的函数可以调用Java层的函数。

Java语言的跨平台是因为在不同平台上可以运行Java虚拟机,而虚拟机是跑在具体平台上的,而本质上Java是通过JNI技术实现的跨平台,很多基层的模块在Java语言诞生之前已经有了比较优秀的实现,为了避免重复造轮子所以我们要使用JNI技术来使用已有的模块。JNI技术与Java应运而生,它也是推动Java快速发展的真正原因。

实现过程

在弄清楚JNI实现过程之前,先给大家介绍一个查看Android系统源码的网站

查看Android系统源码

接下来结合邓凡平的《深入理解Android 卷1》中的MediaScanner的案例来分析。

Java层

/frameworks/base/media/java/android/media/MediaScanner.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MediaScanner
{
//加载对应的so库
static {
System.loadLibrary("media_jni");
native_init();
}

//声明native方法
private native void processDirectory(String path, String extensions, MediaScannerClient client);
private native void processFile(String path, String mimeType, MediaScannerClient client);
public native void setLocale(String locale);

public native byte[] extractAlbumArt(FileDescriptor fd);

private static native final void native_init();
private native final void native_setup();
private native final void native_finalize();
}

可以看到MediaScanner.java中有很多native方法声明,这些方法的实现是被定义到动态库libmedia_jni.so中的。

JNI层对应的是libmedia_jni.so。media_jni就是JNI库的名字,下划线前的media是native库的名字,jni表示他是一个JNI库。Android平台默认采用“lib模块名_jni.so”的命名方式。

原则上来讲加载JNI库可以在使用native方法之前的任何时候任何地方加载,通常的做法是在static代码块中加载。

Native层

以上面processDirectory的native方法声明为例,打开native实现

/frameworks/base/media/jni/android_media_MediaScanner.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
27
28
29
30
31
32
static void
android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject ent)
{
MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

if (path == NULL) {
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
if (extensions == NULL) {
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}

const char *pathStr = env->GetStringUTFChars(path, NULL);
if (pathStr == NULL) { // Out of memory
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);
if (extensionsStr == NULL) { // Out of memory
env->ReleaseStringUTFChars(path, pathStr);
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}

MyMediaScannerClient myClient(env, client);
mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);
env->ReleaseStringUTFChars(path, pathStr);
env->ReleaseStringUTFChars(extensions, extensionsStr);
}

android_media_MediaScanner_processDirectory对应的是上面的processDirectory方法,对应规则就是包名加类名并替换“.”为“_”

注册JNI函数

当Java层调用native_init函数时,会从对应的JNI库中寻找Java_android_media_MediaScanner_native_init函数,如果没有会报错。如果找到会建立一个关联关系,其实就是保存JNI层的函数指针,后面再次调用该方法直接使用这个函数指针,保存关联关系是又虚拟机来完成的。

上面的MediaScanner通过对应规则建立的关系我们叫静态注册,这种注册有几个弊端:

  1. 所有native方法必须生成对应的javah头文件。
  2. javah生成的JNI层函数名特别长。
  3. 初次调用native的时候需要建立关联关系,会影响运行效率。

解决这几个弊端的方法就是动态注册,通过手动关联起来。用来记录这种一一对应关系的是JNINativeMethod结构。

JNINativeMethod结构体定义

1
2
3
4
5
typedef struct {
const char* name; //java中的native函数名
const char* signature; //java函数的签名信息,是参数和返回值类型的组合
void* fnPtr; //JNI层对应的函数指针,注意他是void*类型
} JNINativeMethod;

怎么使用这个结构体呢?

/frameworks/base/media/jni/android_media_MediaScanner.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int register_android_media_MediaScanner(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaScanner", gMethods, NELEM(gMethods));
}

static JNINativeMethod gMethods[] = {
{"processDirectory", "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void *)android_media_MediaScanner_processDirectory},
{"processFile", "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void *)android_media_MediaScanner_processFile},
{"setLocale", "(Ljava/lang/String;)V", (void *)android_media_MediaScanner_setLocale},
{"extractAlbumArt", "(Ljava/io/FileDescriptor;)[B", (void *)android_media_MediaScanner_extractAlbumArt},
{"native_init", "()V", (void *)android_media_MediaScanner_native_init},
{"native_setup", "()V", (void *)android_media_MediaScanner_native_setup},
{"native_finalize", "()V", (void *)android_media_MediaScanner_native_finalize},
};

AndroidRuntime类提供了一个registerNativeMethods方法来完成动态注册工作。现在的问题是这个register_android_media_MediaScanner方法是什么时候被调用的呢?

当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找库中一个叫JNI_OnLoad函数,如果有则调用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;

if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);

if (register_android_media_MediaScanner(env) < 0) {
LOGE("ERROR: MediaScanner native registration failed\n");
goto bail;
}

//....省略
}

数据类型

Java和Native的基础数据类型之间的转换关系如下:

Java Native 符号属性 字长
boolean jboolean 无符号 8bit
byte jbyte 无符号 8bit
char jchar 无符号 16bit
short jshort 无符号 16bit
int jint 有符号 32bit
long jlong 有符号 64bit
float jfloat 有符号 32bit
double jdouble 有符号 64bit

Java和Native的引用类型之间的转换关系如下:

Java Native
Object类型 jobject
java.lang.Class实例 jclass
java.lang.String实例 jstring
Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
java.lang.Throwable实例 jthrowable
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray

除了java基本类型的数组、Class、String和Throwable外,其余数据类型在JNI中都用jobject表示。

1
private native void processFile(String path, String mimeType, MediaScannerClient client);

上面native方法对应的JNI的参数声明如下

1
2
3
4
5
6
static void
android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
//...
}

可以看到后面三个参数和native方法的String、MediaScannerClient对应。

JNIEnv对象

上面我们已经看到每个native方法所对应的JNI函数都有一个JNIEvent指针参数。

1
2
3
4
5
6
7
8
9
10
11
12
/*
* C++ object wrapper.
*
* This is usually overlaid on a C struct whose first element is a
* JNINativeInterface*. We rely somewhat on compiler behavior.
*/
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;

//...结构体内容太多,不方便贴出来
}

JNIEnv是一个与线程相关的代表JNI环境的结构体。实际上是提供了一些JNI系统函数,通过这些函数可以做到调用Java的函数、操作jobject对象等。

通过JNIEnv操作jobject

一个Java对象是由成员属性和方法组成,所以在JNIEnv中定义了两个函数来分别对应Java中的属性和方法。

/dalvik/libnativehelper/include/nativehelper/jni.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;

#if defined(__cplusplus)

//....

jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }

jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }

//....
}

jclass代表java类,name代表成员函数或成员变量的名字,sig为这个函数和变量的签名信息。

/frameworks/base/media/jni/android_media_MediaScanner.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");
if (mediaScannerClientInterface == NULL) {
fprintf(stderr, "android/media/MediaScannerClient not found\n");
}
else {
mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile",
"(Ljava/lang/String;JJ)V");
mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface, "handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
mSetMimeTypeMethodID = env->GetMethodID(mediaScannerClientInterface, "setMimeType",
"(Ljava/lang/String;)V");
mAddNoMediaFolderMethodID = env->GetMethodID(mediaScannerClientInterface, "addNoMediaFolder",
"(Ljava/lang/String;)V");
}
}

通过上面的两个方法会获得对应的属性和方法的id(jmethodID和jfieldID),如果每次操作jobject前都去查询id将会影响效率,所以我们初始化的时候可以取出这些id并保存。

接下来看看如何调用scanFile方法。

1
2
3
4
5
6
7
8
9
// returns true if it succeeded, false if an exception occured in the Java code
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
mEnv->DeleteLocalRef(pathStr);
return (!mEnv->ExceptionCheck());
}

可以看出来,通过JNIEnv的CallVoidMethod把jobject、jMethodID等对应参数传进去,JNI层就能调用Java函数了。

JNI类型签名

上面我们看到也多次提到sig为这个函数和变量的签名信息,为什么函数的参数需要这个信息呢?

因为Java支持函数重载,我们只是单纯的根据函数名无法确定是同一个函数,为了解决这个问题,JNI中将参数类型和返回值类型组合成了一个函数的签名信息,使用函数名和签名信息就可以唯一的确定一个函数。

常见的类型标识符如下:

类型标识 Java类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L/java/languageString; String
[I int[]
[L/java/lang/object; Object[]

如果Java类型是数组则标识符中会有一个“[”,另外引用类型标识符后面会有一个“;”

函数签名的小例子:

函数签名 Java函数
()Ljava/lang/String; String f()
(ILjava/langClass;)J long f(int i, Class c)
([B)V void f(byte[] bytes)

垃圾回收

在JNI中使用下面语句是不会增加引用计数的,所以使用的对象可能已经被回收。

1
save_this = thiz;

JNI中提供了三种类型的引用:

  1. Local Reference : 本地引用,一旦JNI层函数返回,这些object就可能被垃圾回收。
  2. Global Reference : 全局引用,不主动释放,永远不会被垃圾回收。
  3. Weak Global Reference : 弱全局引用,在运行过程中可能会被垃圾回收,使用前需要用JNIEnv的IsSameObject判断。

全局引用使用

1
2
3
4
5
6
7
8
9
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)), //使用NewGlobalRef创建全局引用
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
//....
}

在构造函数中创建了全局引用,在析构函数中释放全局引用。

1
2
3
4
virtual ~MyMediaScannerClient()
{
mEnv->DeleteGlobalRef(mClient); //释放全局引用
}

本地引用使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// returns true if it succeeded, false if an exception occured in the Java code
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
jstring pathStr;

//使用NewStringUTF创建了一个本地引用的String对象
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

//当执行完CallVoidMethod方法后pathStr对象会被回收。
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);

//既然上面说了pathStr会回收,这个DeleteLocalRef函数是不是多余的呢?
mEnv->DeleteLocalRef(pathStr);
return (!mEnv->ExceptionCheck());
}

上面代码清晰的展示了本地引用的使用过程,但是留下了一个疑问,我们试想假设scanFile方法被循环执行了10000次,那么pathStr是不是不能立即被回收,如果不主动调用DeleteLocalRef函数可能会造成内存泄漏。