![](https://img.haomeiwen.com/i15234279/1c866e9ad679b91c.png)
在 Android 的世界里,JNI 的重要性可以说是无可代替,虽然平常我们用到的很少,但是了解它的实现原理是让你穿梭在上下层之间的保障!来自小马哥的博客,这篇及十分的深刻,希望能帮助到大家.
核心源码
关键类 | 路径 |
---|---|
cMediaScanner.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 | aframeworks/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 voidandroid_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 voidandroid_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 // 必须包含这个头文件,否则编译通不过
/* 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在本线程的执行环境。
JNUEnv的作用
✨ 调用 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 style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: inherit; word-wrap: break-word !important;">Method系列函数。</type>
所以,我们可以看出,虽然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成员和函数的签名信息。
垃圾回收及异常处理
这部分我打算单独放在一篇博文中探讨,结果具体错误进行分析。
免费获取更多安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总可以加:936332305 / 链接:点击链接加入【安卓开发架构】:https://jq.qq.com/?_wv=1027&k=515xp64
![](https://img.haomeiwen.com/i15234279/da01a7dafcc80891.png)
网友评论