ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
正如代码中注释的那样,native_init函数对应的JNI函数是android_media_MediaScanner_native_init,可是细心的读者可能要问了,你怎么知道native_init函数对应的是这个android_media_MediaScanner_native_init,而不是其他的呢?莫非是根据函数的名字? 大家知道,native_init函数位于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 machinegenerated */ #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 JNICALLJava_android_media_MediaScanner_processFile (JNIEnv *, jobject, jstring,jstring, jobject); ......//略去一部分注释内容 //native_init对应的JNI函数 JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init (JNIEnv*, jclass); #ifdef __cplusplus } #endif #endif ~~~ 从上面代码中可以发现,native_init和processFile的JNI层函数被声明成: ~~~ //Java层函数名中如果有一个”_”的话,转换成JNI后就变成了”_l”。 JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile ~~~ 需解释一下,静态方法中native函数是如何找到对应的JNI函数的。其实,过程非常简单: - **当Java层调用native_init函数时,它会从对应的JNI库Java_android_media_MediaScanner_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“。 constchar* name; //Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。 const char* signature; void* fnPtr; //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中的哪个类 returnAndroidRuntime::registerNativeMethods(env, "android/media/MediaScanner", gMethods, NELEM(gMethods)); } ~~~ AndroidRunTime类提供了一个registerNativeMethods函数来完成注册工作,下面看registerNativeMethods的实现,代码如下: **AndroidRunTime.cpp** ~~~ int AndroidRuntime::registerNativeMethods(JNIEnv*env, constchar* className, const JNINativeMethod* gMethods, int numMethods) { //调用jniRegisterNativeMethods函数完成注册 returnjniRegisterNativeMethods(env, className, gMethods, numMethods); } ~~~ 其中jniRegisterNativeMethods是Android平台中,为了方便JNI使用而提供的一个帮助函数,其代码如下所示: **JNIHelp.c** ~~~ int jniRegisterNativeMethods(JNIEnv* env, constchar* className, constJNINativeMethod* gMethods, int numMethods) { jclassclazz; clazz= (*env)->FindClass(env, className); ...... //实际上是调用JNIEnv的RegisterNatives函数完成注册的 if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { return -1; } return0; } ~~~ 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(JavaVM* vm, void* reserved) { //该函数的第一个参数类型为JavaVM,这可是虚拟机在JNI层的代表喔,每个Java进程只有一个 //这样的JavaVM JNIEnv* env = NULL; jintresult = -1; if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { gotobail; } ...... //动态注册MediaScanner的JNI函数。 if(register_android_media_MediaScanner(env) < 0) { goto bail; } ...... returnJNI_VERSION_1_4;//必须返回这个值,否则会报错。 } ~~~ JNI函数注册的内容介绍完了。下面来关注JNI技术中其他的几个重要部分。 * * * * * **注意** :JNI层代码中一般要包含jni.h这个头文件。Android源码中提供了一个帮助头文件JNIHelp.h,它内部其实就包含了jni.h,所以我们在自己的代码中直接包含这个JNIHelp.h即可。 * * * * *