版权声明:本文为博主原创文章,转载请注明出处http://blog.csdn.net/u013132758。 https://blog.csdn.net/u013132758/article/details/51425123
经过一周多的时间,这篇博客终于码完了。这篇博客主要是为了帮助大家深入理解JNI,写这篇博客主要是因为前段时间去面试,面试有道25分的简答题就是让我概述Android JNI 的调用过程,当时我就懵逼了,我除了知道JNI是“Java native interface”的缩写外,其他什么都不知道。好了,废话少说,下面为大家开始介绍JNI:
1、JNI概述
JNI 是 Java Native Interfa 的缩写,中文译为“Java 本地调用”。通俗的说JNI是一种技术,通过这种技术我们可以做到以下两点:
1)Java 程序中的函数可以调用 Native 语言写的函数,Nativ 一般指的是C/C++编写的函数;
2)Native 中的函数也可以调用Java层的函数。
再往直白点说就是
可以实现 Java 和 C/C++ 之间的相互调用
。有人可能会问,在平台无关的Java中为什么要创建一个与Native相关的JNI技术呢?这会不会破坏 Java 的平台无关特性。JNI技术推出有以下两个方面的考虑:
1、承载Java世界的虚拟机是用Native语言写的,而虚拟机又运行在具体的平台上,所以虚拟机本身无法做到平台无关。然而有了JNI技术后就可以对Java层屏蔽不同操作系统平台(如Windows和Linux)之间的差异了(例如:打开一个文件,Windows用的是OpenFile函数,而Linux上API用的是open函数)。这样就能实现Java本身的平台无关性。其实Java一直在用JNI技术,只是我们平时很少用到罢了。
2、早在Java语言诞生之前,大多数程序都是用Native语言写的,他们遍布在软件世界的各个角落。Java出世之后,迅速发展,但无法将整个软件世界彻底改朝换代,于是乎就有了折中的办法。既然已经有Native模块实现了相关功能,那么在Java中使用JNI技术调用他们不就行了。另外,一些要求效率和速度的场合还是需要Native语言参与的。
在Android平台上JNI技术就是一座将Native世界和 Java世界链接起来的桥梁,下图明确展示了JNI在Android平台所处的位置:
从上图我们可以知道JNI将JAVA世界和Native世界紧密的联系在一起。在Android平台上如果没有JNI技术支持Java我们将寸步难行。
2、通过MediaScanner实例介绍JNI
调用层次关系,图示:
JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数。
2.1JAVA层代码分析:MediaScanner.java
JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数。
2.1JAVA层代码分析:MediaScanner.java
<span style="font-size:14px;">public class MediaScanner
{
static { //static 语句,前面的 JAVA学习系列(一) 有说明过这个问题
//加载对应用JNI库,linux上即调用libmedai_jni.so,而windows平台上调用libmedia_jni.dll
System.loadLibrary("media_jni");
native_init(); //调用native函数
}
//非native函数
public void scanDirectories(String[] directories, String volumeName)
//声明native函数,native为JAVA语言的关键字
private static native final void native_init();
private native final void native_setup();
private native final void native_finalize();
private native void processDirectory(String path, String extensions, MediaScannerClient client);
}
</span>
总结: 两个函数一个是加载jni动态库,另一个是声明java的native函数
2.2 JNI层的MediaScanner分析
MS在JNI层的代码为android_media_MediaScanner.cpp中
<span style="font-size:14px;">//①这个函数是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");
......
return;
}
//这个函数是processFile的JNI层实现。
static void android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,
jstring path, jstring mimeType, jobject client)
{
MediaScanner*mp = (MediaScanner *)env->GetIntField(thiz, fields.context);
......
constchar *pathStr = env->GetStringUTFChars(path, NULL);
......
if(mimeType) {
env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
}
}
</span>
怎么会知道Java层的native_init函数对应的是JNI层的android_media_MediaScanner_native_init函数呢?下面就来回答这个问题。
首先来找下java层的native_init函数对应jni层的函数?
首先native_init函数位于android.media包中,它的全路径:android.media.MediaScanner.native_init,由于
在native语言中,"."有特殊的意义,所以用"_"来替换:即上面路径这为:android_media_MediaScanner_native_init
ok,函数名就如此关联起来了。而JNI函数注册有两种方式:
1、静态注册方法
a、编写java代码,然后编译生成.class文件
b、使用javah -o output packagename.classname生成jni层头文件
调用时先加载动态库,然后查找native_init函数的jni函数:android_media_MediaScanner_native_init
如果找到则建立这两个函数的关联关系,即保存jni层函数的函数指针,这个由虚拟机完成。
缺点:javah生成的函数名特别长,不利于书写,且第一次调用时需要根据函数名字搜索建立关联关系。
2、动态注册方法
利用JNINativeMethod结构保存其关系
typedef struct
{
//JAVA中native函数名字
const char *name;
//签名信息,用字符串表示,参数类型及返回值类型的组合
const char *signature;
///JNI层函数函数指针,转换为void*类型
void *fnPtr;
};
static JNINativeMethod gMethods[] = {
{"processDirectory", "(Ljava/lang/String;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},
{"setLocale", "(Ljava/lang/String;)V", (void *)android_media_MediaScanner_setLocale},
{"extractAlbumArt", "(Ljava/io/FileDescriptor;)[B", (void *)android_media_MediaScanner_extractAlbumArt},
{"native_init", "()V", (void *)android_media_MediaScanner_native_init},
{"native_setup", "()V", (void *)android_media_MediaScanner_native_setup},
{"native_finalize", "()V", (void *)android_media_MediaScanner_native_finalize},
};
这里说明一下签名信息:
因为JAVA支持函数重载,可以定义同名但不同参数的函数,但直接根据函数名是没法找到具体函数的,因此利用参数类型及返回类型
组成签名信息。
常用类型标识符:
类型标识 JAVA类型 字长
Z boolean 8位
B byte 8位
C char 16位 -- 注意哟,定义为unicode码
S short 16位
I int 32位
J long 64位
F float 32位
D double 64位
L/java/languageString String
[I int[] int数组
[L/java/lang/object Object[] 对象数组
// 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)
{
//利用registerNativeMethods注册JNI函数
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaScanner", gMethods, NELEM(gMethods));
}
// 加载jni库,查找该库中的JNI_OnLoad函数完成动态注册工作
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
...
if (register_android_media_MediaScanner(env) < 0) {
LOGE("ERROR: MediaScanner native registration failed\n");
goto bail;
}
}
static void
android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject client)
{
MyMediaScannerClient myClient(env, client);
mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);
...
}
NATIVE库实现:MediaScanner.cpp (编译成libmedia.so库)
status_t MediaScanner::processDirectory(
const char *path, const char *extensions,
MediaScannerClient &client,
ExceptionCheck exceptionCheck, void *exceptionEnv) {
...
client.setLocale(locale());
status_t result =
doProcessDirectory(
pathBuffer, pathRemaining, extensions, client,
exceptionCheck, exceptionEnv);
free(pathBuffer);
...
}
如此整个流程调用逻辑就讲完了。
第二大部分:JNIEnv 介绍
JNIEnv 是一个与线程相关的变量,由于线程相关,所以线程B中不能使用线程A中的JNIEnv函数。
那个多个线程由谁来保存并保证每个线程的JNIEnv结构体正确呢?
jint JNI_OnLoad(JavaVM* vm, void* reserved)
全进程只有一个JavaVM对象,可以保存并在任何地方使用没有问题,独此一份。
利用JavaVM中的 AttachCurrentThread函数,就可以得到这个线程的 JNIEnv结构体,利用用DetachCurrnetThread释放相应资源
a、通过 JNIEnv 操作 jobject
jfieldID 操作成员变量
jmethodID 操作成员函数
/*
* Get a field ID (instance fields).
*/
static jfieldID GetFieldID(JNIEnv* env, jclass jclazz,
const char* name, const char* sig)
/*
* Get a method ID for an instance method.
*
* JNI defines <init> as an instance method, but Dalvik considers it a
* "direct" method, so we have to special-case it here.
*
* Dalvik also puts all private methods into the "direct" list, so we
* really need to just search both lists.
*/
static jmethodID GetMethodID(JNIEnv* env, jclass jclazz, const char* name,
const char* sig)
具体实现参考:dalvik\vm\jni.c的实现:重要的函数还有 JNI_CreateJavaVM [Create a new VM instance]
举例说明JNI如何调用JAVA层函数:
JNIEnv *mEnv;
jmethodID mScanFileMethodID;
jmethodID mHandleStringTagMethodID;
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
...
// 找到 android.media.MediaScannerClient类在JNI层对应用用jclass实例
jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");
//依此取出 MediaScannerClient 类中的函数 scanFile/handleStringTag 的 jMethodID
mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile",
"(Ljava/lang/String;JJ)V");
mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface, "handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
}
// returns true if it succeeded, false if an exception occured in the Java code
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
// 调用JNIEnv的CallVoidMethod函数:
// 第一个参数代表 MediaScannerClient 的 jobject 对象,第二个参数代表 scanFile的 jMethodID
// 后面的是JAVA层 ScanFile 函数参数
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
}
实现上JNIEnv提供了一系列类似的函数调用JAVA函数:
NativeType Call<type>Method(JNIEnv *env, jobject obj,jmethodID methodID,...)
同时也可以设定或获取JAVA层的成员变量值,定义如下:
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID);
首先native_init函数位于android.media包中,它的全路径:android.media.MediaScanner.native_init,由于
在native语言中,"."有特殊的意义,所以用"_"来替换:即上面路径这为:android_media_MediaScanner_native_init
ok,函数名就如此关联起来了。而JNI函数注册有两种方式:
1、静态注册方法
a、编写java代码,然后编译生成.class文件
b、使用javah -o output packagename.classname生成jni层头文件
调用时先加载动态库,然后查找native_init函数的jni函数:android_media_MediaScanner_native_init
如果找到则建立这两个函数的关联关系,即保存jni层函数的函数指针,这个由虚拟机完成。
缺点:javah生成的函数名特别长,不利于书写,且第一次调用时需要根据函数名字搜索建立关联关系。
2、动态注册方法
利用JNINativeMethod结构保存其关系
typedef struct
{
//JAVA中native函数名字
const char *name;
//签名信息,用字符串表示,参数类型及返回值类型的组合
const char *signature;
///JNI层函数函数指针,转换为void*类型
void *fnPtr;
};
static JNINativeMethod gMethods[] = {
{"processDirectory", "(Ljava/lang/String;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},
{"setLocale", "(Ljava/lang/String;)V", (void *)android_media_MediaScanner_setLocale},
{"extractAlbumArt", "(Ljava/io/FileDescriptor;)[B", (void *)android_media_MediaScanner_extractAlbumArt},
{"native_init", "()V", (void *)android_media_MediaScanner_native_init},
{"native_setup", "()V", (void *)android_media_MediaScanner_native_setup},
{"native_finalize", "()V", (void *)android_media_MediaScanner_native_finalize},
};
这里说明一下签名信息:
因为JAVA支持函数重载,可以定义同名但不同参数的函数,但直接根据函数名是没法找到具体函数的,因此利用参数类型及返回类型
组成签名信息。
常用类型标识符:
类型标识 JAVA类型 字长
Z boolean 8位
B byte 8位
C char 16位 -- 注意哟,定义为unicode码
S short 16位
I int 32位
J long 64位
F float 32位
D double 64位
L/java/languageString String
[I int[] int数组
[L/java/lang/object Object[] 对象数组
// 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)
{
//利用registerNativeMethods注册JNI函数
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaScanner", gMethods, NELEM(gMethods));
}
// 加载jni库,查找该库中的JNI_OnLoad函数完成动态注册工作
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
...
if (register_android_media_MediaScanner(env) < 0) {
LOGE("ERROR: MediaScanner native registration failed\n");
goto bail;
}
}
static void
android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject client)
{
MyMediaScannerClient myClient(env, client);
mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);
...
}
NATIVE库实现:MediaScanner.cpp (编译成libmedia.so库)
status_t MediaScanner::processDirectory(
const char *path, const char *extensions,
MediaScannerClient &client,
ExceptionCheck exceptionCheck, void *exceptionEnv) {
...
client.setLocale(locale());
status_t result =
doProcessDirectory(
pathBuffer, pathRemaining, extensions, client,
exceptionCheck, exceptionEnv);
free(pathBuffer);
...
}
如此整个流程调用逻辑就讲完了。
第二大部分:JNIEnv 介绍
JNIEnv 是一个与线程相关的变量,由于线程相关,所以线程B中不能使用线程A中的JNIEnv函数。
那个多个线程由谁来保存并保证每个线程的JNIEnv结构体正确呢?
jint JNI_OnLoad(JavaVM* vm, void* reserved)
全进程只有一个JavaVM对象,可以保存并在任何地方使用没有问题,独此一份。
利用JavaVM中的 AttachCurrentThread函数,就可以得到这个线程的 JNIEnv结构体,利用用DetachCurrnetThread释放相应资源
a、通过 JNIEnv 操作 jobject
jfieldID 操作成员变量
jmethodID 操作成员函数
/*
* Get a field ID (instance fields).
*/
static jfieldID GetFieldID(JNIEnv* env, jclass jclazz,
const char* name, const char* sig)
/*
* Get a method ID for an instance method.
*
* JNI defines <init> as an instance method, but Dalvik considers it a
* "direct" method, so we have to special-case it here.
*
* Dalvik also puts all private methods into the "direct" list, so we
* really need to just search both lists.
*/
static jmethodID GetMethodID(JNIEnv* env, jclass jclazz, const char* name,
const char* sig)
具体实现参考:dalvik\vm\jni.c的实现:重要的函数还有 JNI_CreateJavaVM [Create a new VM instance]
举例说明JNI如何调用JAVA层函数:
JNIEnv *mEnv;
jmethodID mScanFileMethodID;
jmethodID mHandleStringTagMethodID;
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
...
// 找到 android.media.MediaScannerClient类在JNI层对应用用jclass实例
jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");
//依此取出 MediaScannerClient 类中的函数 scanFile/handleStringTag 的 jMethodID
mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile",
"(Ljava/lang/String;JJ)V");
mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface, "handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
}
// returns true if it succeeded, false if an exception occured in the Java code
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
// 调用JNIEnv的CallVoidMethod函数:
// 第一个参数代表 MediaScannerClient 的 jobject 对象,第二个参数代表 scanFile的 jMethodID
// 后面的是JAVA层 ScanFile 函数参数
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
}
实现上JNIEnv提供了一系列类似的函数调用JAVA函数:
NativeType Call<type>Method(JNIEnv *env, jobject obj,jmethodID methodID,...)
同时也可以设定或获取JAVA层的成员变量值,定义如下:
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID);
NativeTYpe Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value);
补充说明:
如何生成静态Java的native jni头文件?
1、先编写java代码,使用eclipse编译生成.class文件
2、使用javah程序生成头文件
javah -o output.h packagename.classname 举例如下:
C:\Program Files\Java\jdk1.6.0_23\bin>javah -o output -classpath F:\JoinWSP\Json
Test\bin\classes test.Android.json.DirectStruct
这里有几点注意下:
output 是生成的头文件
-classpath <路径> 用于装入类的路径
packagename.classname 完整的包名