JNI是JAVA语言和C/C++(native)语言之间互相调用的接口。
如何使用JNI
JAVA调用C/C++
使用JNI还是很方便,主要分两步:
1. 加载C/C++生成的动态库
如 System.loadLibrary("media_jni"),就是要加载libmedia_jni.so动态库。
2. 声明native函数
如 private static native final void native_init(),这里面有个修饰符是native,说明该函数是native库里面的函数。
C/C++调用JAVA
我们以MediaRecorder的一个应用为例,录像的文件大小可以设置,在录制的过程中,当达到设置的大小时,C++部分就会通知Java应用程序,这个过程就是C++调用Java函数。这个回调Java主要的处理环节在android_media_MediaRecorder.cpp的JNIMediaRecorderListener:: notify函数:
void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
{
LOGV("JNIMediaRecorderListener::notify");
JNIEnv *env = AndroidRuntime::getJNIEnv();
env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, 0);
}
该函数完成两个任务
1. 获得JNI的环境参数JNIEnv;
2. 通过CallStaticVoidMethod函数调用Java函数,这个函数就是mClass的fields.post_event方法,后面的其他参数为Java的post_event函数的参数;
这样就可以调用Java某个类的方法了。
可以看出,在这里面最重要的一个中间传递者就是JNIEnv,它可以获得Java层类对象以及类对象的方法和成员,对于更详细介绍JNIEnv,请链接到JNIEnv小节。下面是android_media_MediaRecorder.cpp的获得类和类方法和成员的代码:
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Can't find android/media/MediaRecorder");
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "I");
if (fields.context == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Can't find MediaRecorder.mNativeContext");
return;
}
fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");
if (fields.surface == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Can't find MediaRecorder.mSurface");
return;
}
jclass surface = env->FindClass("android/view/Surface");
if (surface == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Can't find android/view/Surface");
return;
}
fields.surface_native = env->GetFieldID(surface, ANDROID_VIEW_SURFACE_JNI_ID, "I");
if (fields.surface_native == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Can't find Surface.mSurface");
return;
}
fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
"(Ljava/lang/Object;IIILjava/lang/Object;)V");
if (fields.post_event == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "MediaRecorder.postEventFromNative");
return;
}
}
1. 通过FindClass获得了android/media/MediaRecorder类;
2. 通过GetFieldID获得了android/media/MediaRecorder类的mNativeContext和mSurface成员;
3. 通过FindClass获得了android/view/Surface类;
4. 通过GetFieldID获得了android/view/Surface类的mNativeSurface成员;5. 通过GetStaticMethodID获得了postEventFromNative方法;
Java native 函数和C/C++函数的挂钩
虽然,JNI用起来很简单,如何才能将Java声明的native函数和实际的C/C++函数挂钩呢?通过查询,有两种办法,Android常用的是动态注册,还有一个静态注册方法。
动态注册
我们以android_media_MediaRecorder.cpp为例,MediaRecoder.java声明了“public native void start()”函数,该start函数要和android_media_MediaRecorder.cpp文件的android_media_MediaRecorder_start函数挂钩,他俩是如何联系到一起的呢。
通过查阅资料,当Java加载动态库后,它会执行该库的JNI_OnLoad函数,该函数包含如下几行代码:
if (register_android_media_MediaRecorder(env) < 0) {
LOGE("ERROR: MediaRecorder native registration failed\n");
goto bail;
}
通过函数名字是注册MediaRecorder,register_android_media_MediaRecorder(env) 定义如下
int register_android_media_MediaRecorder(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaRecorder", gMethods, NELEM(gMethods));
}
从中,有两个参量很重要,一个是 "android/media/MediaRecorder",它是Java程序的相应的类;一个是gMethods结构体数组,这个数组包含很多如下格式的结构体 {"start", "()V", (void *)android_media_MediaRecorder_start},从表面意思就是将Java的"android/media/MediaRecorder"类的start函数和android_media_MediaRecorder_start函数挂钩,这样就实现了注册。AndroidRuntime::registerNativeMethods定义如下
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
/*
* Register native JNI-callable methods.
*
* "className" looks like "java/lang/String".
*/
int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
LOGV("Registering %s natives\n", className);
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'\n", className);
return -1;
}
int result = 0;
if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
LOGE("RegisterNatives failed for '%s'\n", className);
result = -1;
}
(*env)->DeleteLocalRef(env, clazz);
return result;
}
/*
* Register one or more native functions in one class.
*
* This can be called multiple times on the same method, allowing the
* caller to redefine the method implementation at will.
*/
static jint RegisterNatives(JNIEnv* env, jclass jclazz,
const JNINativeMethod* methods, jint nMethods)
{
JNI_ENTER();
ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(env, jclazz);
jint retval = JNI_OK;
int i;
if (gDvm.verboseJni) {
LOGI("[Registering JNI native methods for class %s]\n",
clazz->descriptor);
}
for (i = 0; i < nMethods; i++) {
if (!dvmRegisterJNIMethod(clazz, methods[i].name,
methods[i].signature, methods[i].fnPtr))
{
retval = JNI_ERR;
}
}
JNI_EXIT();
return retval;
}
以上的步骤基本如下:
1. 通过传入的Class字符串查找Java中是否有对应的类;
2. 通过Java虚拟机提供的注册函数接口,将gMethods数组的函数对注册到虚拟机中;
这样就完成了动态注册。
静态注册
静态注册方法是通过函数名字进行挂钩的方法,如Java start函数,那么它就到对应的动态库中寻找名字为“android_media_MediaRecorder_start”的函数,其中“android_media_MediaRecordert”表示对应的Java类名,_start表示函数的名字,这样做的方法有以下几点弊端,同时也是动态注册的优点:
1. C/C++定义的函数名字必须遵循这样的规定,否则两个函数无法挂钩;
2. 第一次调用函数时,必须到动态库中搜索这个函数,会影响速度,以后就会将对应的函数地址记录下来,所以第二次及以后使用该函数就和动态注册效率一致;
JNIEnv
在上面两节都用到了JNIEnv,看来通过它可以获得Java层有什么类对象,以及类对象的成员和方法;可以将Java层的native函数和C/C++函数挂钩,那么这个JNIEnv是如何来的呢?
在android_media_MediaPlayer.cpp中的JNI_OnLoad函数:
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_MediaPlayer(env) < 0) {
LOGE("ERROR: MediaPlayer native registration failed\n");
goto bail;
}
1. JNI_OnLoad第一个参数是JavaVM,这表示和java虚拟机有关的参量,一个进程只有一个这个参数;
2. 通过vm->GetEnv就可以获得JNIEnv参量,通过JNIEnv参量就可以让Java获得C/C++对应的函数;让C/C++获得Java相应的方法和成员变量;
JNIEnv就是JNI技术的关键。JNIEnv再向下执行,就是调用Java虚拟提供的功能接口了,我们就不作分析了,我只要懂得怎么使用JNI技术即可,我们要明白,我们既可以在Java层获得C/C++程序,也可以在C/C++层调用Java程序。
引用计数
JNI共提供了三种引用计数,和C++的引用计数还有很大的不同。
Local Reference
本地引用,jni层函数使用的非全局引用对象都是local reference,包括函数调用时传入的jobject和在JNI层函数中创建的jobject。
当所在函数退出时,这些jobject才可能被垃圾回收。
上面传达了两层意思:
1. 函数退出时,本地引用不一定马上回收;
2. 函数没退出时,本地引用不会被回收;
所以,当出现类似下面的情况时,
For(int i = 0; i < 100; i++){
Jstring pathStr = mEnv->NewStringUTF(path);
//do something
}
在函数退出前,会出现100个pathStr,所以内存可能很快被消耗完,那怎么办呢?
如果在每个循环里面调用env->DeleteLocalRef(pathStr),那么当执行完一个循环,pathStr就会被回收。
所以,DeleteLocalRef是本地引用一个很重要的操作。
Global Reference
如下面的一段JNI代码:
Android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz,jstring path, jstring mimeType, jobject client){
。。。。。。;
//保存Java层换入的jobject对象,代表MediaScanner对象
Save_this = thiz;
。。。。。。;
Return;
}
这里面是有问题的,因为JNI里面Save_this = thiz这句话是不会改变引用计数的,所以,如果其他函数有调用Save_this的地方,可能对应的thiz对象已经被释放了。那怎么办呢?
JNI提供了全局引用,全局引用对象如不主动释放,它永远不会被垃圾回收。
所以如
Save_this = (jclass)env->NewGlobalRef(thiz);
将Save_this设为全局引用,那么对应的thiz对象是不会被释放的。
只有当调用env->DeleteGlobalRef(Save_this)时,对应的thiz对象才有可能被回收,否则永远不会被释放。