【Android 系统开发】_“核心技术”篇 -- JNI
开篇
核心源码
关键类 | 路径 |
---|---|
MediaScanner.java | frameworks/base/media/java/android/media/MediaScanner.java |
android_media_MediaScanner.cpp | frameworks/base/media/jni/android_media_MediaScanner.cpp |
android_media_MediaPlayer.cpp | frameworks/base/media/jni/android_media_MediaPlayer.cpp |
AndroidRuntime.cpp | frameworks/base/core/jni/AndroidRuntime.cpp |
JNI概述
JNI是Java Native Interface的缩写,中文译为“Java本地调用”,通俗地说,JNI是一种技术,通过这种技术可以做到以下两点:
✨ Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写函数;
✨ Native程序中的函数可以调用Java层的函数,也就是说在C/C++程序中可以调用Java的函数;
在平台无关的Java中,为什么要创建一个与Native相关的JNI技术呢?这岂不是破坏了Java的平台无关特性吗?JNI技术的推出主要有以下几个方面的考虑:
✨ 承载Java世界的虚拟机是用Native语言写的,而虚拟机又运行在具体的平台上,所以虚拟机本身无法做到平台无关。然而,有了JNI技术后,就可以对Java层屏蔽不同操作系统平台之间的差异了。这样,就能实现Java本身的平台无关特性。
✨ 在Java诞生之前,很多程序都是用Native语言写的,随后Java后来受到追捧,并且迅速发展,但是作为一门高级语言,无法将软件世界彻底的改变。那么既然Native模块实现了许多功能,那么在Java中直接通过JNI技术去使用它们不久可以了?
所以,我们可以把JNI看作一座将Native世界和Java世界互联起来的一座桥梁(特殊说明:JNI层的代码也是用Native写的哦!)。
原理图如下:
一律的讲原理很枯燥,我们直接以实际的代码作为范例来学习JNI的原理和实际使用!
MediaScanner
如果你是做Android系统开发和维护工作的,那么你肯定听过MediaScanner,那我们就拿它来举例,看看它和JNI之间是如何关联的。
(MediaScanner是Android平台中多媒体系统的重要组成部分,它的功能是扫描媒体文件,得到诸如歌曲时长、歌曲作者等媒体信息,并将他们存入到媒体数据库中,拱其他应用程序使用。)
MediaScanner和它的JNI:
我们简单说明下这个流程图:
✨ Java世界对应的是MediaScanner,而这个MediaScanner类有一些函数需要由Native层来实现(定义了一些Native函数,具体实现代码在Native层)
✨ JNI层对应的是libmedia_jni.so。
· media_jni是JNI库的名字,其中下划线前的“media”是Native层库的名字,这里就是libmedia库。下划线后的“jni”表示它是一个JNI库。
· Android平台基本上都采用“lib模块名_jni.so”来命名JNI库。
✨ Native层对应的是libmedia.so,这个库完成了实际的功能。
✨ MediaScanner将通过JNI库libmedia_jni.so和Native层的libmedia.so交互。
源码分析 - Java层
MediaScanner.java
我们先来看看MediaScanner在Java层中关于JNI的代码:
package android.media; public class MediaScanner implements AutoCloseable { static { // static语句 // 这个我们之前说过,media_jni为JNI库的名字,实际加载动态库的时候会将其拓展成libmedia_jni.so System.loadLibrary("media_jni"); native_init(); // 调用native_init函数 } ... ... private native void processFile(String path, String mimeType, MediaScannerClient client); ... ... private static native final void native_init(); // 申明一个native函数。native为Java的关键字,表示它由JNI层实现。 ... ... }
OK,以上代码列出了两个重要的要点:(1)加载JNI库;(2)调用Java的native函数
加载JNI库
我们前面说到过,如果Java要调用native函数,就必须通过一个位于JNI层的动态库来实现。那么这个动态库在什么时候、什么地方加载?
原则上,在调用native函数之前,我们可以在任何时候、任何地方去加载动态库。但一般通行的做法就是在类的static语句中加载,调用System.loadLibrary方法就可以了。
native函数
我们发现native_init和processFile函数前面都有Java的关键字native,这个就表示函数将由JNI层来实现。
所以在Java层面去使用JNI只要做两项工作:(1)加载对应的JNI库;(2)申明由关键字native修饰的函数。
源码分析 - JNI层
实现函数
接下来我们看下Java层中定义的两个native函数在JNI层的实现。
native_init的JNI层实现
static const char* const kClassMediaScanner = "android/media/MediaScanner"; static void android_media_MediaScanner_native_init(JNIEnv *env) { jclass clazz = env->FindClass(kClassMediaScanner); if (clazz == NULL) { return; } fields.context = env->GetFieldID(clazz, "mNativeContext", "J"); if (fields.context == NULL) { return; } }
processFile的JNI层实现
static void android_media_MediaScanner_processFile( JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client) { // Lock already hold by processDirectory MediaScanner *mp = getNativeScanner_l(env, thiz); ... ... const char *pathStr = env->GetStringUTFChars(path, NULL); ... ... env->ReleaseStringUTFChars(path, pathStr); if (mimeType) { env->ReleaseStringUTFChars(mimeType, mimeTypeStr); } }
这边我们来解答一个问题,我们确实是知道MediaScanner的native函数是JNI层去实现的,但是系统是如何知道Java层的native_init函数对应的就是JNI层的android_media_MediaScanner_native_init函数呢?
注册JNI函数
不知道你有没有注意到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。
我们知道了Java层native函数对应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。
/* 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_linit(JNIEnv *, jclass); #ifdef _cplusplus } #endif #endif
从上面代码中可以发现,native_init和processFile的JNI层函数被声明成:
// Java 层函数名中如果由一个“_”, 转换成JNI后就变成了“l” JNIEXPORT void JNICALL Java_android_media_MediaScanner_processFile JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_linit
Ok,那么静态方法中native函数是如何找到对应的JNI函数的呢?
当Java层调用native_init函数时,它会从对应的JNI库中寻找Java_android_media_MediaScanner_native_init函数,如果没有,就会报错。如果找到,则会为这个native_init和Java_android_media_MediaScanner_native_init建立一个关联关系,其实就是保存JNI层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。
从这里可以看出,静态方法就是根据函数名来建立Java函数与JNI函数之间的关联关系的,而且它要求JNI函数的名字必须遵循特定的格式。
这种方法有三个弊端,如下:
✨ 需要编译所有声明了native函数的Java类,每个所生成的class文件都得用javah生成一个头文件;
✨ javah生成的JNI层函数名特别长,书写起来很不方便;
✨ 初次调用native函数时需要根据函数名称搜索对应的JNI函数来建立关联关系,这样会影响运行效率。
所以我们是否有办法克服以上三点弊端?我们知道静态方法是去动态库里找一遍,然后建立关联关系,以后再根据这个函数指针去调用对应的JNI函数,那么如果我们直接让native函数直接知道JNI层对应函数的函数指针,是不就Ok了?
这就是下面我们要介绍的第二种方法:动态注册法!
动态方法注册
我们知道Java native函数和JNI函数是一一对应的,这个就像我们key-value一样,那么如果有一个结构来保存这种关联关系,那么通过这个结构直接可以找到彼此的关联,是不是就效率就高多了?
答案是肯定的,动态注册就是这么干的!在JNI技术中,用来记录这种一一对应关系的,是一个叫 JNINativeMethod 的结构,其定义如下:
typedef struct { char *name; // Java中native函数的名字,不用携带包的路径,例如:native_init char *signature; // Java中函数的签名信息,用字符串表示,是参数类型和返回值类型的集合 void *fnPtr; // JNI层对应函数的函数指针,注意它是 void* 类型 }JNINativeMethod;
下面我们看看如何使用这个结构体,看下MediaScanner JNI层是如何做的。
// 定义一个JNINativeMethod数组,其成员就是MediaScanner中所有native函数的一一对应关系。 static const JNINativeMethod gMethods[] = { ... ... { "processFile", // Java中native函数的函数名 "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V", // processFile的签名信息 (void *)android_media_MediaScanner_processFile // JNI层对应的函数指针 }, ... ... { "native_init", "()V", (void *)android_media_MediaScanner_native_init }, ... ... };
是不是很一目了然?定义好了,不能直接用啊,当然需要注册一下。
// This function only registers the native methods, and is called from // JNI_OnLoad in android_media_MediaPlayer.cpp // 注册JNINativeMethod数组 int register_android_media_MediaScanner(JNIEnv *env) { // 调用AndroidRuntime的registerNativeMethods函数,第二个参数表明是Java中的哪个类 // 我们在讲解Zygote原理时,聊过创建Java虚拟机,注册JNI函数的内容 return AndroidRuntime::registerNativeMethods(env, kClassMediaScanner, gMethods, NELEM(gMethods)); }
AndroidRunTime类提供了一个registerNativeMethods函数来完成注册的工作,下面来看下registerNativeMethods的实现:
/* * Register native methods using JNI. */ /*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { // 调用jniRegisterNativeMethods函数完成注册 return jniRegisterNativeMethods(env, className, gMethods, numMethods); }
其实,jniRegisterNativeMethods是Android平台中为了方便JNI使用而提供的一个帮助函数,其代码如下:
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; } }
我知道你看到这边已经头疼了,调用来调用去,看上去很麻烦,是不是?其实动态注册的工作,只用两个函数就能完成,如下:
(1)jclass clazz = (*env)->FindClass(env, className);
env指向一个JNIEnv结构体,它非常重要,后面我们会讨论。classname为对应的Java类名,由于JNINativeMethod中使用的函数名并非全路径名,所以要指明是哪个类。
(2)(*env)->RegisterNatives(env, clazz, gMethods, numMethods);
调用JNIEnv的RegisterNatives函数,注册关联关系。
那么,你现在知道了如果动态注册了,但是有个问题,这些动态注册的函数在什么时候和什么地方被调用?
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着就会去查找该库中一个叫JNI_OnLoad的函数。如果有,就调用它,而动态注册的工作就是在这里完成的。
JNI_OnLoad
动态库是libmedia_jni.so,那么JNI_OnLoad函数在哪里实现的?如果你看的比较自信的话,我相信之前代码中有段注释你应该注意到了。
// 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, kClassMediaScanner, gMethods, NELEM(gMethods)); }
由于多媒体系统很多地方都使用了JNI,所以JNI_OnLoad被放到了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; } ... ... if (register_android_media_MediaScanner(env) < 0) { ALOGE("ERROR: MediaScanner native registration failed\n"); goto bail; } ... ... /* success -- return valid version number */ result = JNI_VERSION_1_4; bail: return result; }
数据类型转换
在Java中调用native函数传递的参数是Java数据类型,那么这些参数类型传递到JNI层会变成什么类型?
Java数据类型分为“基本数据类型”和“引用数据类型”两种,JNI层也是区别对待两者的。
基本数据类型的转换
Java基本类型 | Native类型 | 符号属性 | 字长 |
---|---|---|---|
boolean | jboolean | 无符号 | 8位 |
byte | jbyte | 无符号 | 8位 |
char | jchar | 无符号 | 16位 |
short | jshort | 有符号 | 16位 |
int | jint | 有符号 | 32位 |
long | jlong | 有符号 | 64位 |
float | jfloat | 有符号 | 32位 |
double | jdoublt | 有符号 | 64位 |
引用数据类型的转换
Java引用类型 | Native类型 | Java引用类型 | Native类型 |
---|---|---|---|
All objects | jobject | char[] | jcharArray |
java.lang.Class 实例 | jclass | short[] | jshortArray |
java.lang.String 实例 | jstring | int[] | jintArray |
Object[] | jobjectArray | long[] | jlongArray |
boolean[] | jbooleanArray | float | jfloatArray |
byte[] | jbyteArray | double[] | jdoubleArray |
java.lang.Throwable 实例 | jthrowable |
我们举例说明,看下processFile函数:
private native void processFile ( String path, String mimeType, MediaScannerClient client); static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
我们发现:
✨ Java的String类型在JNI层对应为jstring类型;
✨ Java的MediaScannerClient类型在JNI层对应为jobject。
不知道你有没有注意到一个问题,Java中的processFile中只有三个参数,为什么到了JNI层对应的函数却有五个参数?
static void android_media_MediaScanner_processFile( JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
接下来我们开始重点讨论JNIEnv!!!
JNIEnv
JNIEnv的概念
是一个与线程相关的代表JNI环境的结构体,该结构体代表了Java在本线程的执行环境。
JNIEnv的作用
✨ 调用 Java 函数 : JNIEnv 代表 Java 执行环境, 能够使用 JNIEnv 调用 Java 中的代码
✨ 操作 Java 对象 : Java 对象传入 JNI 层就是 Jobject 对象, 须要使用 JNIEnv 来操作这个 Java 对象
我们来看一个有趣的现象
前面,我们已经知道 JNIEnv 是一个与线程相关的变量,如果此时线程 A 有一个 JNIEnv 变量, 线程 B 也有一个JNIEnv变量,由于线程相关,所以 A 线程不能使用 B 线程的 JNIEnv 结构体变量。
此时,一个java对象通过JNI调用动态库中的一个send()函数向服务器发送消息,不等服务器消息到来就立即返回,同时把JNI接口的指针JNIEnv *env(虚拟机环境指针),和jobject obj保存在动态库中的变量里。一段时间后,动态库中的消息接收线程接收到服务器发来的消息,并试图通过保存过的env和obj来调用先前的java对象的方法(相当于JAVA回调方法)来处理此消息,此时程序突然退出(崩溃)。
为什么?
原因:前台JAVA线程发送消息,后台线程处理消息,归属于两个不同的线程,不能使用相同的JNIEnv变量。
怎么解决?
还记得我们前面介绍的JNI_OnLoad函数吗?它的第一个参数是JavaVM,它是虚拟机在JNI层的代表!!!
// 全进程只有一个JavaVM对象,所以可以在任何地方使用 jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
那么也就是说,不论进程有多少线程(不论有多少JNIEnv),JavaVM却是独此一份!所以,我们可以利用一个机制:利用全局的 JavaVM 指针得到当前线程的 JNIEnv 指针。
JavaVM和JNIEnv:
✨ 调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体,这样就可以在后台线程中回调Java函数了。
✨ 另外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。
通过JNIEnv操作jobject
前面介绍数据类型的时候,我们知道Java的引用类型除了少数几个外(Class、String和Throwable),最终在JNI层都会用jobject来表示对象的数据类型,那么该如何操作这个jobject呢?
我们先回顾下Java对象是由什么组成的?当然是它的成员变量和成员函数了!那么同理,操作jobject的本质就应当是操作这些对象的成员变量和成员函数!那么jobject的成员变量和成员函数又是什么?
取出jfieldID和jmethodID
在java中,我们知道成员变量和成员函数都是由类定义的,他们是类的属性,那么在JNI规则中,也是这么来定义的,用jfieldID定义Java类的成员变量,用jmethodID定义Java类的成员函数。
可通过JNIEnv的下面两个函数得到:
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig) jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
其中,jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息(后面会说到)。
我们来看看在MediaScanner中如何使用它们,直接看代码:android_media_MediaScanner.cpp::MyMediaScannerClient构造函数
class MyMediaScannerClient : public MediaScannerClient { public: MyMediaScannerClient(JNIEnv *env, jobject client)... ... { // 先找到android.media.MediaScannerClient类在JNI层中对应的jclass实例 jclass mediaScannerClientInterface = env->FindClass(kClassMediaScannerClient); if (mediaScannerClientInterface == NULL) { ALOGE("Class %s not found", kClassMediaScannerClient); } 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"); ... ... } } ... ... jobject mClient; jmethodID mScanFileMethodID; jmethodID mHandleStringTagMethodID; ... ... }
从上面的代码中,将scanFile和handleStringTag函数的jMethodID保存在MyMediaScannerClient的成员变量中。为什么这里要把它们保存起来呢?这个问题涉及到一个关于程序运行效率的知识点:
如果每次操作jobject前都要去查询jmethodID或jfieldID,那么将会影响程序运行的效率,所以我们在初始化的时候可以取出这些ID并保存起来以供后续使用。
使用jfieldID和jmethodID
我们来看看android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile函数
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; } /* * 调用JNIEnv的CallVoidMethod函数 * 注意CallVoidMethod的参数: *(1)第一个参数是代表MediaScannerClient的jobject对象 *(2)第二个参数是函数scanFile的jmethodID,后面是Java中的scanFile的参数 */ mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize, isDirectory, noMedia); mEnv->DeleteLocalRef(pathStr); return checkAndClearExceptionFromCallback(mEnv, "scanFile"); }
通过JNIEnv输出CallVoidMethod,再把jobject、jMethodID和对应的参数传进去,JNI层就能够调用Java对象的函数了!
实际上JNIEnv输出了一系列类似CallVoidMethod的函数,形式如下:
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...)
其中type对应java函数的返回值类型,例如CallIntMethod、CallVoidMethod等。如果想调用Java中的static函数,则用JNIEnv输出的CallStatic<Type>Method系列函数。
所以,我们可以看出,虽然jobject是透明的,但有了JNIEnv的帮助,还是能轻松操作jobject背后的实际对象的。
jstring
这一节我们单独聊聊String。Java中的String也是引用类型,不过由于它的使用频率很高,所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。
虽然jstring是一种独立的数据累心,但是它并没有提供成员函数以便操作。而C++中的string类是由自己的成员函数的。那么该如何操作jstring呢?还是得依靠JNIEnv提供帮助。
先看几个有关jstring的函数:
✨ 调用JNIEnv的NewString(const jchar* unicodeChars, jsize len),可以从Native的字符串得到一个jstring对象。
✨ 调用JNIEnv的NewStringUTF(const char* bytes)将根据Native的一个UTF-8字符串得到一个jstring对象。
✨ 上面两个函数将本地字符串转换成了Java的String对象,JNIEnv还提供了GetStringChars函数和GetStringUTFChars函数,它们可以将Java String对象转换成本地字符串。其中GetStringChars得到一个Unicode字符串,而GetStringUTFChars得到一个UTF-8字符串。
✨ 另外,如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用ReleaseStringChars函数或ReleaseStringUTFChars函数来对应地释放资源,否认会导致JVM内存泄漏。
我们看段代码加深印象:
static void android_media_MediaScanner_processFile( JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client) { // Lock already hold by processDirectory MediaScanner *mp = getNativeScanner_l(env, thiz); ... ... const char *mimeTypeStr = (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL); if (mimeType && mimeTypeStr == NULL) { // Out of memory // ReleaseStringUTFChars can be called with an exception pending. env->ReleaseStringUTFChars(path, pathStr); return; } ... ... }
JNI类型签名
我们看下动态注册中的一段代码:
static const JNINativeMethod gMethods[] = { ... ... { "processFile", // processFile的签名信息,这么长的字符串,是什么意思? "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V", (void *)android_media_MediaScanner_processFile }, ... ... };
上面这段代码我们之前早就见过了,不过"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V"是什么意思呢?
我们前面提到过,这个是Java中对应函数的签名信息,由参数类型和返回值类型共同组成,有人可能有疑问,这东西是干嘛的?
我们都知道,Java支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名是没法找到具体函数的。为了解决这个问题,JNI技术中就将参数类型和返回值类型的组合作为一个函数的签名信息,有了签名信息和函数名,就能很顺利地找到Java中的函数了。
JNI规范定义的函数签名信息看起来很别扭,不过习惯就好了。它的格式是:
(参数 1 类型标识参数 2 类型标识 ... 参数 n 类型标识) 返回值类型标识
我们仍然拿processFile的例子来看下:
{ "processFile", // Java中的函数定义为 private native void processFile(String path, String mimeType, MediaScannerClient client); // 对应的JNI函数签名如下: "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V", // void类型对应的标示是V // 当参数的类型是引用类型时,其格式是“L包名”,其中包名中的“.”换成“/”,Ljava/lang/String表示是一个Java String类型 (void *)android_media_MediaScanner_processFile },
【注意】:引用类型(除基本类型的数组外)的标识最后都有一个“;”。
函数签名不仅看起来麻烦,写起来更麻烦,稍微写错一个标点都会导致注册失败,所以在具体编码时,可以定义字符串宏(这边就不多做解释了,可以自行查询了解即可)。
虽然函数签名信息很容易写错,但是Java提供了一个叫javap的工具能够帮助我们生成函数或变量的签名信息,它的用法如下:
javap -s -p xxx
其中 xxx 为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印所有函数和成员的签名信息,默认只会打印public成员和函数的签名信息。
垃圾回收及异常处理
这部分我打算单独放在一篇博文中探讨,结果具体错误进行分析。