2.4 JNI层MediaScanner的分析

MediaScanner(简称“MS”)的JNI层代码在android_media_MediaScanner.cpp中,如下所示:


[—>android_media_MediaScanner.cpp]

//①这个函数是native_init的JNI层实现。

static void android_media_MediaScanner_native_init(JNIEnv*env)

{

jclass clazz;

clazz=env->FindClass("android/media/MediaScanner");

……

fields.context=env->GetFieldID(clazz,"mNativeContext","I");

……

return;

}

//这个函数是processFile的JNI层实现。

static void android_media_MediaScanner_processFile(JNIEnv*env,jobject thiz,

jstring path,jstring mimeType,jobject client)

{

MediaScannermp=(MediaScanner)env->GetIntField(thiz,fields.context);

……

const char*pathStr=env->GetStringUTFChars(path,NULL);

……

if(mimeType){

env->ReleaseStringUTFChars(mimeType,mimeTypeStr);

}

}


上面是MS的JNI层代码,不知道大家看了以后是否会产生一些疑惑?

我想最大的疑惑可能是,如何才能知道Java层的native_init函数对应的是JNI层的android_media_MediaScanner_native_init函数呢?下面就来回答这个问题。

2.4.1 注册JNI函数

正如代码中注释的那样,native_init函数对应的JNI函数是android_media_MediaScanner_native_init,可能细心的读者要问了,你怎么知道native_init函数对应的是这个JNI函数,而不是其他的呢?莫非是根据函数的名字来确定的?

大家知道,nativeinit函数位于android.media这个包中,它的全路径名应该是android.media.MediaScanner.native_init,而JNI层函数的名字是android_media_MediaScanner_native_init。因为在Native语言中,符号“.”有着特殊的意义,所以JNI层需要把Java函数名称(包括包名)中的“.”换成“”。也就是通过这种方式,native_init找到了自己JNI层的本家兄弟android.media.MediaScanner.native_init。

上面其实讨论的是JNI函数的注册问题,“注册”之意就是将Java层的native函数和JNI层对应的实现函数关联起来,有了这种关联,调用Java层的native函数时,就能顺利转到JNI层对应的函数执行了。而JNI函数的注册方法实际上有两种,下面分别做介绍。

1.静态方法

我们从网上找到的与JNI有关的资料,一般都会介绍如何使用这种方法来完成JNI函数的注册,这种方法就是根据函数名来找对应的JNI函数。它需要Java的工具程序javah参与,整体流程如下:

先编写Java代码,然后编译生成.class文件。

使用Java的工具程序javah,如javah-o output packagename.classname,这样它会生成一个叫output.h的JNI层头文件。其中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。

这个头文件的名字一般都会使用packagename_class.h的样式,例如MediaScanner对应的JNI层头文件就是android_media_MediaScanner.h。下面来看这种方式生成的头文件:


[—>android_media_MediaScanner.h:样例文件]

/DO NOT EDIT THIS FILE-it is machine generated/

include<jni.h>//必须包含这个头文件,否则编译通不过

/Header for class android_media_MediaScanner/

ifndef_Included_android_media_MediaScanner

define_Included_android_media_MediaScanner

ifdef__cplusplus

extern"C"{

endif

……略去一部分内容

//processFile的JNI函数

JNIEXPORT void JNICALL Java_android_media_MediaScanner_processFile

(JNIEnv*,jobject,jstring,jstring,jobject);

……//略去一部分内容

//native_init对应的JNI函数

JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_1init

(JNIEnv*,jclass);

ifdef__cplusplus

}

endif

endif


从上面代码中可以发现,native_init和processFile的JNI层函数被声明成:


//Java层函数名中如果有一个"_",转换成JNI后就变成了"_l"。

JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_1init

JNIEXPORT void JNICALL Java_android_media_MediaScanner_processFile


需要解释一下静态方法中native函数是如何找到对应的JNI函数的。其实,过程非常简单:

当Java层调用native_init函数时,它会从对应的JNI库中寻找Java_android_media_Media-Scanner_native_linit函数,如果没有,就会报错。如果找到,则会为这个native_init和Java_android_media_MediaScanner_native_linit建立一个关联关系,其实就是保存JNI层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。

从这里可以看出,静态方法就是根据函数名来建立Java函数和JNI函数之间的关联关系的,而且它要求JNI层函数的名字必须遵循特定的格式。这种方法也有几个弊端,即:

需要编译所有声明了native函数的Java类,每个所生成的class文件都得用javah生成一个头文件。

javah生成的JNI层函数名特别长,书写起来很不方便。

初次调用native函数时要根据函数名字搜索对应的JNI层函数来建立关联关系,这样会影响运行效率。

有什么办法可以克服上面三个弊端吗?根据上面的介绍可知,Java native函数是通过函数指针来和JNI层函数建立关联关系的。如果直接让native函数知道JNI层对应函数的函数指针,不就万事大吉了吗?这就是下面要介绍的第二种方法:动态注册法。

2.动态注册

既然Java native函数和JNI函数是一一对应的,那么是不是会有一个结构来保存这种关联关系呢?答案是肯定的。在JNI技术中,用来记录这种一一对应关系的,是一个叫JNINativeMethod的结构,其定义如下:


typedef struct{

//Java中native函数的名字,不用携带包的路径,例如“native_init”。

const char*name;

//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。

const char*signature;

voidfnPtr;//JNI层对应函数的函数指针,注意它是void类型。

}JNINativeMethod;


应该如何使用这个结构体呢?来看MediaScanner JNI层是如何做的,代码如下所示:


[—>android_media_MediaScanner.cpp]

//定义一个JNINativeMethod数组,其成员就是MS中所有native函数的一一对应关系。

static JNINativeMethod gMethods[]={

……

{

"processFile"//Java中native函数的函数名。

//processFile的签名信息,签名信息的知识后面再做介绍。

"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",

(void*)android_media_MediaScanner_processFile//JNI层对应的函数指针。

},

……

{

"native_init",

"()V",

(void*)android_media_MediaScanner_native_init

},

……

};

//注册JNINativeMethod数组

int register_android_media_MediaScanner(JNIEnv*env)

{

//调用AndroidRuntime的registerNativeMethods函数,第二个参数表明是Java中的哪个类return AndroidRuntime:registerNativeMethods(env,"android/media/MediaScanner",gMethods,NELEM(gMethods));

}


AndroidRunTime类提供了一个registerNativeMethods函数来完成注册工作,下面来看registerNativeMethods的实现,代码如下:


[—>AndroidRunTime.cpp]

int AndroidRuntime:registerNativeMethods(JNIEnv*env,

const charclassName,const JNINativeMethodgMethods,int numMethods)

{

//调用jniRegisterNativeMethods函数完成注册return jniRegisterNativeMethods(env,className,gMethods,numMethods);

}


其中jniRegisterNativeMethods是Android平台中为了方便JNI使用而提供的一个帮助函数,其代码如下所示:


[—>JNIHelp.c]

int jniRegisterNativeMethods(JNIEnvenv,const charclassName,

const JNINativeMethod*gMethods,int numMethods)

{

jclass clazz;

clazz=(*env)->FindClass(env,className);

……

//实际上是调用JNIEnv的RegisterNatives函数完成注册的if((*env)->RegisterNatives(env,clazz,gMethods,numMethods)<0){

return-1;

}

return 0;

}

wow,好像很麻烦啊!其实动态注册的工作,只用两个函数就能完成。总结如下:

/*

env指向一个JNIEnv结构体,它非常重要,后面会讨论它。classname为对应的Java类名,由于JNINativeMethod中使用的函数名并非全路径名,所以要指明是哪个类。

*/

jclass clazz=(*env)->FindClass(env,className);

//调用JNIEnv的RegisterNatives函数,注册关联关系。

(*env)->RegisterNatives(env,clazz,gMethods,numMethods);


所以,在自己的JNI层代码中使用这种方法,就可以完成动态注册了。这里还有一个很棘手的问题:这些动态注册的函数在什么时候和什么地方被调用呢?这里就不卖关子了,直接给出该问题的答案:

当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数。如果有,就调用它,而动态注册的工作就是在这里完成的。

所以,如果想使用动态注册方法,就必须实现JNI_OnLoad函数,只有在这个函数中才有机会完成动态注册的工作。静态注册的方法则没有这个要求,但建议大家也实现这个JNI_OnLoad函数,因为有一些初始化工作是可以在这里做的。

那么,libmedia_jni.so的JNI_OnLoad函数是在哪里实现的呢?由于多媒体系统很多地方都使用了JNI,所以“码农”把它放到android_media_MediaPlayer.cpp中了,代码如下所示:


[—>android_media_MediaPlayer.cpp]

jint JNI_OnLoad(JavaVMvm,voidreserved)

{

//该函数的第一个参数类型为JavaVM,这可是虚拟机在JNI层的代表喔,每个Java进程只有一个

//这样的JavaVM。

JNIEnv*env=NULL;

jint result=-1;

if(vm->GetEnv((void**)&env,JNI_VERSION_1_4)!=JNI_OK){

goto bail;

}

……//动态注册MediaScanner的JNI函数。

if(register_android_media_MediaScanner(env)<0){

goto bail;

}

……

return JNI_VERSION_1_4;//必须返回这个值,否则会报错。

}


JNI函数注册的相关内容介绍完了。下面来关注一下JNI技术中其他的几个重要部分。

注意 JNI层代码中一般要包含jni.h这个头文件。Android源码中提供了一个帮助头文件JNIHelp.h,它内部其实就包含了jni.h,所以我们在自己的代码中直接包含这个JNIHelp.h即可。