《深入理解Android(卷1)》笔记 1.第二章 深入理解JNI
2012-09-08 00:31 ...平..淡... 阅读(948) 评论(0) 编辑 收藏 举报第一章就跳过了,比较基础。
第二章 深入理解JNI
知识点1:JNI概述
JNI:Java Native Interface,中译为“java本地接口”。Native语言一般指C/C++。
JNI技术可以实现java与C/C++之间的互通(java程序<----调用---->C/C++程序)。
虽然java语言是平台无关的,但JNI的推出基于以下几个方面的考虑:
(1)java虚拟机是Native语言写的,所以虚拟机本身是平台相关的。而有了JNI技术,就可以对java层屏蔽不同操作系统间的差异了。java程序只管通过调用Native方法实现相应功能就行,无需理会平台差异,就实现了java本身的平台无关性。(ps:书中说“其实java一直在使用JNI技术,只是我们平时较少用到罢了”)
(2)在一些要求效率和速度的场合还是需要Native语言参与其中的。
这也就说明了JNI技术的作用: Java世界 <------> JNI层 <-------> Native世界
知识点2:通过分析MediaScanner源码来学习JNI
首先来看一下涉及到的部分:
Java | (MediaScanner) |
JNI | (libmedia_jni.so) |
Native | (libmedia.so) |
分析:
* Java层 对应的是MediaScanner类。 这个类有一些函数需要Native层来实现。 * JNI层 对应的是libmedia_jni.so。 medi_jni是JNI库的名字(Android一般用“lib模块名_jni.so”的命名方式来命名JNI库的名字)。 * Native层 对应的是libmedia.so。 这个库完成了实际的功能。
MediaScanner将通过JNI库libmedia_jni.so和Native层的libmedia.so交互。
Java层的MediaScanner分析
public class MediaScanner { static { //加载JNI库 System.loadLibrary("media_jni"); native_init(); } ...... //声明native函数。native是java的关键字,表示它将由JNI层完成 private native void processDirectory(String path, MediaScannerClient client); private native void processFile(String path, String mimeType, MediaScannerClient client); private static native final void native_init();
所以要想使用JNI,只需做两件事:
1.加载JNI库
2.声明由关键字native修饰的函数
JNI层的MediaScanner分析
JNI注册的两种方法
(1)静态方法:根据函数名来找对应的JNI函数,找到后,就建立一个关联关系(即保存JNI层函数的函数指针)。以后调用就直接使用这个函数指针(这项工作由虚拟机完成)。
ps:如何确定java中的native函数对应的JNI函数:java中native的全路径名是包名.类名.方法名(如MediaScanner类中native_init方法的全路径名是android.media.MediaScanner.native_init)。又因为"."符号在Native语言中有特殊意义,故JNI中,将"."换成"_"。故native_init方法对应的JNI层方法为android_media_MediaScanner_native_init.
静态注册的流程:
javac HelloJNI.java //生成了HelloJNI.class文件
3.生成C语言头文件
javah -jni HelloJNI //生成HelloJNI.h文件。
4.编写C代码
gcc -I "/usr/lib/jvm/java-6-sun-1.6.0.26/include" -I "/usr/lib/jvm/java-6-sun-1.6.0.26/include/linux" -shared hellojni.c -o libhellojni.so
gcc -I "<JDK_HOME>/include" -I"<JDK_HOME>/include/linux" -shared hellojni.c -o libhellojni.so
6.运行Java程序
java -Djava.library.path=. HelloJNI
//—Djava.library.path这个参数必须设置(设置生成的动态库的位置),否则会报找不到动态库。
代码如下:
//编写HelloJNI.java class HelloJNI { native void printHello(); native void printString(String str); static {System.loadLibrary("hellojni");} public static void main(String args[]) { System.out.println(System.getProperty("java.library.path")); HelloJNI myJNI = new HelloJNI(); myJNI.printHello(); myJNI.printString("Hello world!"); // System.out.println(System.getProperty("java.library.path")); } } //通过javah生成的HelloJNI.h /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloJNI */ #ifndef _Included_HelloJNI #define _Included_HelloJNI #ifdef __cplusplus extern "C" { #endif /* * Class: HelloJNI * Method: printHello * Signature: ()V */ JNIEXPORT void JNICALL Java_HelloJNI_printHello (JNIEnv *, jobject); /* * Class: HelloJNI * Method: printString * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_HelloJNI_printString (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif //将HelloJNI.h中的函数,在hellojni.c中实现 #include "HelloJNI.h" #include <stdio.h> JNIEXPORT void JNICALL Java_HelloJNI_printHello (JNIEnv *env, jobject obj) { printf("hello,world in printHello\n"); return; } /* * Class: HelloJNI * Method: printString * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_HelloJNI_printString (JNIEnv *env, jobject obj, jstring string) { const char *str = (*env)->GetStringUTFChars(env,string,0); printf("%s\n",str); return; }
--------------------------------------------------------------------------------------
(2)动态方法:直接让native函数知道JNI层对应函数的函数指针。
动态注册
通过JNINativeMethod 结构体来存储native函数和JNI函数之间的关联关系。
typedef struct { //Java中native函数的名字,不用携带包的路径,例如“native_init” const char* name; //Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合 const char* signature; //JNI层对应函数的函数指针,注意它是void* 类型 void* fnPtr; } JNINativeMethod;
JNINativeMethod在android.media.MediaScanner.cpp中的使用
//1、创建关系对应数组
static JNINativeMethod gMethods[] = { { "processDirectory", "(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 }, ...... { "native_init", "()V", (void *)android_media_MediaScanner_native_init }, ...... }; ......
......
// 2、通过register_android_media_MediaScanner方法来注册JNINativeMethod数组
// This function only registers the native methods, and is called from JNI_OnLoad in android_media_MediaPlayer.cpp int register_android_media_MediaScanner(JNIEnv *env) { return AndroidRuntime::registerNativeMethods(env, "android/media/MediaScanner", gMethods, NELEM(gMethods)); }
该方法调用AndroidRunTime.cpp提供的registerNativeMethods方法,如下所示
/* * Register native methods using JNI. */ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { return jniRegisterNativeMethods(env, className, gMethods, numMethods); }
该方法又会调用jniRegisterNativeMethods方法,该方法在dalvik/libnativehelper/JNIHelp.c中,它是Android为方便JNI使用而提供的一个帮助函数。如下所示
/* * 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); // 1 if (clazz == NULL) { LOGE("Native registration unable to find class '%s'\n", className); return -1; } if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { // 2 LOGE("RegisterNatives failed for '%s'\n", className); return -1; } return 0; }
动态注册的工作,主要就是通过两个函数完成:
(1)FindClass(env, className):找到对应的java类。
(2)RegisterNatives(env, clazz, gMethods, numMethods):注册关联关系。
那动态注册的函数什么时候和什么地方被调用呢?
the result is 当Java层通过System.loadLibrary加载完JNI动态库后,接着会查找该库中的JNI_OnLoad函数,如果有,就调用它,动态注册的工作就是在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; } if (register_android_media_MediaRecorder(env) < 0) { LOGE("ERROR: MediaRecorder native registration failed\n"); goto bail; } //动态注册MediaScanner的JNI函数 if (register_android_media_MediaScanner(env) < 0) { LOGE("ERROR: MediaScanner native registration failed\n"); goto bail; } if (register_android_media_MediaMetadataRetriever(env) < 0) { LOGE("ERROR: MediaMetadataRetriever native registration failed\n"); goto bail; } if (register_android_media_AmrInputStream(env) < 0) { LOGE("ERROR: AmrInputStream native registration failed\n"); goto bail; } if (register_android_media_ResampleInputStream(env) < 0) { LOGE("ERROR: ResampleInputStream native registration failed\n"); goto bail; } if (register_android_media_MediaProfiles(env) < 0) { LOGE("ERROR: MediaProfiles native registration failed"); goto bail; } if (register_android_mtp_MtpDatabase(env) < 0) { LOGE("ERROR: MtpDatabase native registration failed"); goto bail; } if (register_android_mtp_MtpDevice(env) < 0) { LOGE("ERROR: MtpDevice native registration failed"); goto bail; } if (register_android_mtp_MtpServer(env) < 0) { LOGE("ERROR: MtpServer native registration failed"); goto bail; } /* success -- return valid version number */ result = JNI_VERSION_1_4; bail: return result; }
知识点3: 数据类型转换
在java中调用的native函数,传递的参数都是java数据类型,而到了JNI层,则需要有所转换。
java层--->JNI层 数据类型转换方式如下:
(1)基本数据类型的转换:数据类型前加上"j"(如boolean--->jboolean、int--->jint)。
(2)引用数据类型的转换:除了java中基本数据类型的数组、Class、String和Throwable 外,其余所有Java对象的数据类型在JNI中都用jobject表示。
看看processFile这个函数:
java层的processFile函数(有三个参数):
processFile(String path, String mimeType, MediaScannerClient client);
JNI层中对应的函数(最后三个参数与processFile中对应):
android_media_MediaScanner_processFile( JNIEnv *env, //见知识点4 jobject thiz, //表示Java层的MediaScanner对象。它表示在哪个MediaScanner对象上调用的processFile。(如果Java层是static函数,那么这个参数将是jclass,表示是在调用哪个java类的静态函数) jstring path, jstring mimeType, jobject client //这三个参数与processFile的参数对应 (java中的MediaScannerClient类型在JNI中对应为jobject).
);
知识点4:JNIEnv介绍
JNIEnv是一个与线程相关的代表JNI环境的结构体,实际上就是提供了一些JNI系统函数。通过这些系统函数可以做到:
(1)调用java的函数。
(2)操作jobject对象等很多事情。
JavaVM和JNIEnv的关系:
(1)调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调java函数了。
(2)另外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。
java的引用类型,除少数外,在JNI层都是用jobject表示对象的数据类型,那怎么操作jobject呢?答:通过JNIEnv。
其实,操作jobject的本质 就是 操作这些对象的成员变量和成员函数。
操作jobject就两个步骤:
(1)通过jfieldID和jmethodID来表示Java类的成员变量和成员函数。
(2)通过JNIEnv的Call<type>Method方法调用java对象的函数。(其中type对应java的返回值类型)
解释(1):JNI规则下,用jfieldID和jmethodID来表示Java类的成员变量和成员函数,可通过JNIEnv的GetFieldID和GetMethodID两个函数得到(它们的第一个参数都是jclass类型,代表Java类)。
下面看一下MS中的例子:
MyMediaScannerClient(JNIEnv *env, jobject client)...... { //找到android.meida.MediaScanner类在JNI层中对应的jclass实例 jclass mediaScannerClientInterface = env->FindClass(kClassMediaScannerClient); if (mediaScannerClientInterface == NULL) { ...... } else { //取出MediaScannerClient类中函数scanFile的jMethodID mScanFileMethodID = env->GetMethodID( mediaScannerClientInterface, "scanFile", "(Ljava/lang/String;JJZZ)V"); //取出MediaScannerClient类中函数handleStringTag的jMethodID mHandleStringTagMethodID = env->GetMethodID( mediaScannerClientInterface, "handleStringTag", "(Ljava/lang/String;Ljava/lang/String;)V"); //同上 mSetMimeTypeMethodID = env->GetMethodID( mediaScannerClientInterface, "setMimeType", "(Ljava/lang/String;)V"); } }
ps:上面代码中,将scanFile、handleStringTag和setMimeType函数的jmethodID保存为 MyMediaScannerClient的成员变量。主要是为了提高效率,不用每次操作jobject都去查询jmethodID和jfiledID。
解释(2):取出jmethodID后,有什么用呢,该如何使用呢?
其实jmethodID的用处也就是作为JNIEnv的Call<type>Method方法的参数,JNI层将通过Call<type>Method方法调用java对象的函数。
例子:
virtual status_t scanFile(const char* path, long long lastModified, long long fileSize, bool isDirectory, bool noMedia) { ...... jstring pathStr; if ((pathStr = mEnv->NewStringUTF(path)) == NULL) { mEnv->ExceptionClear(); return NO_MEMORY; } /*
第一个参数是代表MediaScannerClient的jobject对象,
第二个参数是函数scanFile的jmethodID,
后面是java函数中scanFile的参数。
*/ mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize, isDirectory, noMedia); mEnv->DeleteLocalRef(pathStr); return checkAndClearExceptionFromCallback(mEnv, "scanFile"); }
知识点5:JNI类型签名介绍
1.类型签名的必要性
Java支持函数重载,因此仅仅根据函数名是无法找到具体函数的,为了解决这个问题,JNI技术引入了类型签名(将参数类型和返回值类型作为一个函数的签名信息)。
2.类型签名的格式
(参数1类型标示;参数2类型表示;......参数n类型标示;)返回值类型标示
例子:(Ljava/lang/String;Landroid/media/MediaScannerClient;)V
ps:当参数类型是引用类型时,其格式为“L包名;”,其中包名中的“.”换成了“/”
知识点6:jstring类型介绍
因为Java中String类型使用频繁,所以JNI规范中单独创建了一个jstring类型来表示Java中的String类型,但jstring类型并没有提供成员函数以便操作。因此还是得依靠JNIEnv。
*调用JNIEnv的NewString(JNIEnv *env, const jchar *unicodeChars,jsize len),可以从Native的字符串得到一个jstring对象。
*调用JNIEnv的NewStringUTF,将根据Native的一个UTF-8字符串得到一个jstring对象。(用到最多)
*上面两个函数将本地字符串转换成了Java的String对象,JNIEnv还提供了GetStringChars函数和GetStringUTFChars函数,用于将Java String对象转换成本地字符串。
*注意,如果在代码中调用了上面几个函数,在做完相关工作后,都需要调用ReleaseStringChars函数或ReleaseStringUTFChars函数来对应地释放资源,否则会导致JVM内存泄露。
知识点7:垃圾回收机制
例子: static jobject save_thiz = null; 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,有问题不? }
肯定会有问题,因为如果java层的thiz已经被回收,则save_thiz保存的很可能是一个野指针了。
JNI提供了三种类型的引用来解决这个问题:
(1)Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和在JNI层函数中创建的jobject。Local Reference最大的特点是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。(用得比较多)(操作完后,可以调用DeleteLocalRef方法,这样本地引用的变量就会被立即回收)
(2)Global Reference:全局引用。这种对象如果不主动释放,它永远不会被垃圾回收。(用得比较多)
(3)Weak Global Reference:弱全局引用。一种特殊的Global Reference,在运行过程中可能会被垃圾回收。所以在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了。
ps:没有及时回收Local Reference可能是造成进程占用内存过多的一个原因,多加注意!
知识点8:JNI中异常处理
JNI中,如果调用JNIEnv的某些函数时出错了,则会产生一个异常,但并不会中断本地函数的执行,只会做一些资源清理的工作。直到从JNI层返回Java层后,虚拟机才会抛出该异常。如果此时又有调用除该出错的JNIEnv的函数外的其他JNIEnv函数,则会导致程序死掉。
JNI曾函数可以在代码中截获和修改这些异常,JNIEnv提供了三个函数:
(1)ExceptionOccured函数,用来判断是否发生异常。
(2)ExceptionClear函数,用来清理当前JNI层中发生的异常。
(3)ThrowNew函数,用来向Java层抛出异常。
OK,总结完毕!第二章断断续续看了蛮长时间,凡是开头难,继续加油!