源码版本:2.3.x
学习JNI实例:MediaScanner
MediaScanner的功能是扫描媒体文件并将它们存储到媒体数据库中,供其他程序使用
--> MediaScanner.java
public class MediaScanner { //加载对应的jni库,库名为libmedia_jni //在实际加载中会扩展成libmedia_jni.so,Windows平台则是media_jni.dll static { System.loadLibrary("media_jni"); native_init(); } //声明一个native函数,native为Java关键字,表示将由JNI层完成 private static native final void Native_init(); }
--> 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"); } }
但是如何确定 Java
层的 native_init
函数对应的就是 JNI
层的 android_media_MediaScanner_native_init
函数。这就涉及到 JNI
函数的注册问题。
所谓 注册 就是将 Java
层声明的 Native
函数与 JNI
层对应的实现函数关联起来。在这个例子中, native_init
函数位于 android.media
包中,全路径名为 android.media.MediaScanner.native_int
,JNI
层函数的名字是 android_media_MediaScanner_native_init
。 JNI
函数的注册方法分为静态注册和动态注册。
JNI
函数。Java
代码,编译生成 .class
文件。使用 Java
的工具程序 javah
,如 javah -o output packagename.classname
。 packagename.classname
是 Java
代码编译后的 class
文件,而在生成的 output.h
文件里,声明了对应的 JNI
层函数,只要实现里面的函数即可。
--> androyid_media_MediaScanner.h::样例文件
#include <jni.h> //必须包含这个文件,否则编译不通过 #ifndef _Included_android_media_MediaScanner #define _Included_android_media_MediaScanner #ifdef __cplusplus extern "C" { #endif //native_init对应的JNI函数 //Java层函数名中如果有一个"_",被转换成JNI之后就变成了"_l"。 JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_linit(JNIEnv *, jclass); } #endif #endif
缺点:
native
函数的类都需要编译并生成头文件 javah
生成的 JNI
层函数名太长 native
函数时要根据函数名搜索对应 JNI
函数,影响效率 动态注册JNI
技术中,有 JNINativeMethod
结构体用来记录 native
函数和 JNI
函数的对应关系。
typedef struct { //Java中native函数的名字,不用携带包的路径,例如"native_init"。 const char* name; //Java函数的签名信息,用字符串表示,时参数类型和返回值类型的组合 const char* signature; void* fnPtr; //JNI层对应函数的函数指针, void* 类型 } JNINativeMethod
MediaScanner JNI
层是这么做的~
--> android_media_MediaScanner.cpp
//定义一个JNINativeMethod数组,其成员就是MS中所有native函数的一一对应关系 static JNINativeMethod gMethods[] = { ······ { "native_init", //java中native函数的函数名 "()V", (void *)android_media_MediaScanner_native_init //JNI层对应的函数指针 }, ······ }; //注册JNINativeMethod数组 int register_android_media_MediaScanner(JNIEnv * env) { //调用AndroidRuntime的registerNativeMethods函数,第二个参数标明是Java中的哪个类 return AndroidRuntime::registerNativeMethods(env, "android/media/MediaScanner", gMethods, NELEM(gMethods)); }
接下来是 registerNativeMethods
的实现
--> AndroidRuntime.cpp
int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { //调用jniRegisterNativeMethods函数完成注册 return jniRegisterNativeMethos(env, className, gMethods, numMethods); }
jniRegisterNativeMethods
是 Android
平台为了方便 JNI
使用而提供的帮助函数。
--> JNIHelp.c
int jniRegisterNativeMethods(JNIEnv* env, const char* className, 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; }
真是多重调用...android_media_MediaScanner::register_android_media_MediaScanner -> AndroidRuntime::registerNativeMethods -> JNIHelp::jniRegisterNativeMethods
但说到最后也就 jniRegisterNativeMethods
中的两步
那么是什么时候完成注册呢?当 Java
层通过 System.loadLibray
加载完 JNI
动态库后,紧接着会查找 JNI_OnLoad
函数,如果有,就调用并进行注册。libmedia_jni.so
的 JNI_OnLoad
函数我们可以在 android_media_MediaPlayer.cpp
中找到。
--> android_media_MediaPlayer.cpp
jint JNI_OnLoad(JavaVM* vm, void* reserved) { //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; }
这里没什么花里胡哨的,就两个表格
可以看到除了 Java
中基本数据类型的数组、Class
、String
、Throwable
外,其余所有的 Java
对象的数据类型在 JNI
中用 jobject
表示。
//Java层 processFile(String path, String mimeType, MediaScannerClient client); //JNI层 android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
JNIEnv
是一个与线程相关的代表JNI环境的结构体。
通过调用JNIEnv的一些JNI系统函数进而可以调用 Java
的函数、操作 jobject
对象等很多事情。
操作 jobject
即是操作该对象的成员变量和成员函数,在 JNI
规则中,用 jfieldID
和 jmethodID
表示。
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig); jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
MS中这样使用它们
--> android_media_MediaScanner.cpp::MyMediaScannerClient构造函数
--> android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile
MyMediaScannerClient(JNIEnv *env, jobject client).... { //先找到android.media.MediaScannerClient类在JNI层中对应的jclass实例 jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient"); //取出MediaScannerClient类中的函数scanFile的jMethodID mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile", "(Ljava/lang/String;JJ)V"); } virtual bool scanFile(const char* path, long long lastModified, long long fileSize) { jstring pathStr; if ((pathStr = mEnv->NewStringUTF(path) == NULL) return false; /* 调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的参数 第一个代表MediaScannerClient的jobject参数 第二个代表函数scanFile的jmethodID,后面是Java中的scanFile的参数 */ mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize); mEnv->DeleteLocalRef(pathStr); return (!mEnv->ExceptionCheck()); }
Java
支持函数重载,即可以定义同名但不同参数的函数。仅仅通过函数名无法找到具体函数。为了解决这个问题,JNI
技术中将参数类型和返回值类型的组合作为了一个函数的签名信息,有了签名信息和函数名,就能很顺利的找到 Java
中的函数。
格式: ( 参数 1 类型标示 参数 2 类型标示 ... 参数 n 类型标示 ) 返回值类型标示
举例: ( Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient; )V
Java
中创建的对象最后由垃圾回收器回收和释放内存。
--> 垃圾回收的例子
static jobject save_thiz = NULL; //定义一个全局的jobject static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client) { ...... //保存Java层传入的jobject对象,代表MediaScanner对象 save_thiz = thiz; ...... return; } //假设在某个时间,有地方调用callMediaScanner函数 void callMediaScanner() { //在这个函数中操作save_thiz,会有问题吗? }
这个做法会有问题,因为和 save_thiz
对应的 java
层中的 MediaScanner
很有可能已经被垃圾回收了,即 save_thiz
保存的 jobject
可能是个野指针,如果使用它,后果会很严重。在被引用中被清理的原因是: JNI
层使用 save_thiz = thiz
这种语句是不会增加 jobject
的引用计数的。
为此, JNI
提供了三种类型的引用:
Local Reference
:本地引用。在 JNI
层函数使用的非全局引用对象都是 Local Reference
,包括函数调用时传入的 jobject
和在 JNI
层函数中创建的 jobject
, Local Reference
最大的特点就是,一旦 JNI
层函数返回,这些 jobject
就可能被垃圾回收。Global Reference
:全局引用,这种对象如不主动释放,它永远不会被垃圾回收。Weak Global Reference
:软全局引用,在运行过程中可能会被垃圾回收,所以在使用之前,需要调用 JNIEnv
的 IsSameObject
判断它是否被回收了。 --> android_media_MediaScanner.cpp::MyMediaScannerClient构造函数
//使用 Global Reference,记得释放。 MyMediaScannerClient(JNIEnv *env, jobject client) : mEnv(env), //调用NewGlobalRef创建一个Global Reference,这样mClient就不用担心被回收了。 mClient(env->NewGlobalRef(client)), mScanFileMethodID(0), mHandStringTagMethodID(0), mSetMimeTypeMethodID(0) { ...... } //析构函数 virtual ~MyMediaScannerClient() { mEnv->DeleteGlobalRef(mClient); //调用DeleteGlobalRef释放这个全局引用 }
调用 JNIEnv
的某些函数出错,会产生异常,但直到返回到 Java
层后才会抛出。异常不会中断本地函数的运行,但是只能做资源清理的工作,如果此时调用其他 JNIEnv
函数,则会导致程序死掉。
--> android_media_MediaScanner.cpp::MyMediaScanner的scanFile函数
virtual bool scanFile(const char* path, long long lastModified, long long fileSize) { jstring pathStr; //NewStringUTF调用失败后,直接返回,不能再干别的事情了 if ((pathStr = nEnv->NewStringUTF([path)) == NULL) return false; ...... }
JNIEnv
提供了三个函数帮助在代码中截获和修改这些异常
ExceptionOccured
函数,用来判断是否发生ExceptionClear
函数,用来清理ThrowNew
函数,用来向 Java
层抛出异常