通过MediaScanner理解JNI原理

参考链接:

《深入理解Android 卷I》

什么是JNI

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

什么是JNI

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方法,对应规则就是包名加类名并替换“.”为“_”

Java方法和Native方法的映射关系

注册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;
}

//....省略
}

可以看到在JNI_OnLoad中调用了register_android_media_MediaScanner方法来实现动态注册。

数据类型

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

JavaNative符号属性字长
booleanjboolean无符号8bit
bytejbyte无符号8bit
charjchar无符号16bit
shortjshort无符号16bit
intjint有符号32bit
longjlong有符号64bit
floatjfloat有符号32bit
doublejdouble有符号64bit

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

JavaNative
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结构

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类型
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L/java/languageString;String
[Iint[]
[L/java/lang/object;Object[]

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

函数签名的小例子:

函数签名Java函数
()Ljava/lang/String;String f()
(ILjava/langClass;)Jlong f(int i, Class c)
([B)Vvoid 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函数可能会造成内存泄漏。

总结

上面完整展示了JNI代码调用的过程,Java代码可以使用声明的native方法(未实现)来调用Native的实现,而Native方法可以通过JNIEnv对象的GetFieldID和GetMethodId方法来获取Java属性和方法来调用Java方法。对比一下会发现整个JNI的过程对Native层的要求比较高,Native层需要根据Java的native方法来注册并实现,而且还需要知道需要调用的Java方法的路径和名字来获取函数指针。

JNI职责关系