本文我们将分析Android的jni机制。
一、JNI 概述
JNI 的全称是 Java Native Interface, 中文名称 “Java本地调用接口”, JNI标准是Java平台的一部分,它允许Java代码能够和其它语言写的代码进行交互。出现JNI技术有以下原因:
- Java语言平台无关,但执行Java语言的虚拟机却是用Native语言写的,与平台相关。出现JNI技术可以向Java层屏蔽平台相关的具体实现细节。
- 编译后的Native语言执行效率更高,将用navtive语言编写的库让java层调用,执行效率更高。
从本质上看,Android平台是由linux操作系统和Dalvik Java虚拟机构成,Dakvik 提供了一个标准的支持JNI调用的Java虚拟机环境。JNI是连接Java层和C/C++部分的纽带。
在Android 4.4.4_r1
源码中,主要的JNI代码放在frameworks\base\core\jni
目录下,查看该目录下的Android.mk
文件,可以发现该目录下的文件被编译成了了libandroid_runtime.so
二、MediaScanner分析
Media系统
使用jni
技术,我们可以分析MediaScanner
来学习JNI
相关技术.涉及的源码有:
frameworks\base\core\jni\AndroidRuntime.cpp
frameworks\base\media\java\android\media\android_media_MediaScanner.cpp
frameworks\base\media\jni\android_media_MediaPlayer.cpp
frameworks\base\media\java\android\media\MediaPlayer.java
libnativehelper\JNIHelp.cpp
MediaScanner
JNI相关部分是:
Java(MediaScanner)
<=>JNI(libmedia_jni.so)
<=>Native(libmedia.so)
2.1 Java层分析
我们首先分析frameworks\base\media\java\android\media\MediaScanner.java
类。
public class MediaScanner{
static {
System.loadLibrary("media_jni");
native_init();
}
......
private static native final void native_init();
private native void processFile(String path, String mimeType, MediaScannerClient client);
......
}
可以看到MediaScanner
类中先加载media_jni.so
共享库,然后调用native_init
方法进行初始化。
总结:
1.Java
层使用Native
层的函数前,需要先加载动态库,通常加载时机是在类的 static
代码块中,调用System.loadLibrary
方法实现,loadLibray
的参数media_jni
是动态库的名称,不用加后缀。
2.Java
中的函数由native
层实现时,需要在方法前添加native
关键字
2.2 JNI层分析
Android的应用层的类都是以Java语言写成,Java类编译为Dex形式的字节码,由Dalvik虚拟机来执行实现。如果Java类需要与C组件进行交互,则由VM载入C组件。VM扮演着桥梁的角色,使Java
语言跟C
语言能够相互沟通。
MediaScanner
的JNI层实现在 frameworks\base\media\jni\android_media_MediaScanner.cpp
文件中
// native_init 的JNI层实现。
// This function gets a field ID, which in turn causes class initialization.
// It is called from a static block in MediaScanner, which won't run until the
// first time an instance of this class is used.
static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
ALOGV("native_init");
jclass clazz = env->FindClass(kClassMediaScanner);
if (clazz == NULL) {
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "I");
if (fields.context == NULL) {
return;
}
}
//processFile的JNI层实现。
static void
android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path,
jstring mimeType, jobject client)
{
ALOGV("processFile");
// Lock already hold by processDirectory
MediaScanner *mp = getNativeScanner_l(env, thiz);
if (mp == NULL) {
jniThrowException(env, kRunTimeException, "No scanner available");
return;
}
if (path == NULL) {
jniThrowException(env, kIllegalArgumentException, NULL);
return;
}
const char *pathStr = env->GetStringUTFChars(path, NULL);
if (pathStr == NULL) { // Out of memory
return;
}
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;
}
MyMediaScannerClient myClient(env, client);
MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
if (result == MEDIA_SCAN_RESULT_ERROR) {
ALOGE("An error occurred while scanning file '%s'.", pathStr);
}
env->ReleaseStringUTFChars(path, pathStr);
if (mimeType) {
env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
}
}
2.2.1 注册JNI函数
Java层的函数名跟Native层的函数名有怎样的对应关系?
通过上面的分析我们知道,native_init
函数对应的JNI函数是android_media_MediaScanner_native_init
。 MediaScanner
类在android.media
包中,native_init
的全路径名为android.media.MediaScanner.native_init
,在Native语言中,.
具有特殊的含义,我们将·_
换成.
即可得到Java
层函数对应的Native
函数。
将Java
层函数与Native
层的函数联系起来的方法有两种,一种是静态注册、另一种是动态注册。
- 静态注册
静态注册流程如下:
1.编写Java
代码,生成.class
文件
2.使用javah
工具,生成packagename_class.h
文件
javah -classpath bin/classes -d jni com.example.testndk.MainActivity
在packagename_class.h
文件中,声明了java类中native关键字方法对应的本地方法,将它们实现即可。
头文件一般使用packagename_class.h
的形式。而生成的函数名为Java_ackagename_class_method
- 动态注册
Java的Native函数与JNI函数是一一对应的关系,JNI技术中,使用 JNINativeMethod 结构体来记录这种一一对应关系。
typedef struct {
const char* name;//Java中native函数名称,不用携带包路径。例如`"native_init"`
const char* signature;//Java函数的签名信息,用字符串表示,是参数类型与返回值的集合
void* fnPtr;//JNI层对应函数的函数指针,注意它是`void*`类型
} JNINativeMethod;
JNI中如何使用这种对应关系,查看frameworks\base\media\jni\android_media_MediaScanner.cpp
可知:
...
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
},
{
"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
},
};
// 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));
}
AndroidRunTime
类提供了一个registerNativeMethods
函数来完成注册
#frameworks\base\core\jni\AndroidRuntime.cpp
/*
* Register native methods using JNI.
*/
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
其中jniRegisterNativeMethods
方法是Android平台为了方便JNI
使用而提供的一个帮助函数,在JNIHelp.cpp
类中。
#libnativehelper\JNIHelp.cpp
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
ALOGV("Registering %s's %d native methods...", className, numMethods);
scoped_local_ref<jclass> c(env, findClass(env, className));
if (c.get() == NULL) {
char* msg;
asprintf(&msg, "Native registration unable to find class '%s'; aborting...", className);
e->FatalError(msg);
}
//实际上是调用JNIEnv 的RegisterNatives
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
char* msg;
asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
e->FatalError(msg);
}
return 0;
}
事实上完成JNI函数的注册,只需要两个函数即可
//env指向一个JNIEnv的结构体,classname为对应的Java类名,由于JNInativeMethod中使用的函数名并非全路径名 ,所以要指明是那个类。
clazz = (*env)->FindClass(env, className);
if ((*env)->RegisterNatives(env, clazz, gMethods,numMethods) < 0){
当Java层通过System.loadLibrary
加载完JNI库后,就会查找JNI_OnLoad
方法,如果有的话,就调用它。所以动态注册在JNI_OnLoad
方法中完成。
libmedia_jni.so
的JNI_OnLoad函数在android_media_MediaPlayer.cpp
中
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);
......
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;
}
2.2.2数据类型的转换
Java中调用native
函数传递的参数类型是Java数据类型,参数类型到了JNI层会变成其他类型数据类型。Java
数据类型分为基本数据和引用数据类型两种,JNI层也是区别对待这两者。
1.基本数据类型的转换
2.引用数据类型的转换
2.2.3 JNIEnv 介绍
JNIEnv
是一个与线程相关的代表JNI环境的结构体。线程A有一个JNIEnv
,线程B有一个JNIEnv
。由于线程相关,所以不能在线程B中使用线程A中的结构体。
正常情况下,Native
函数使用传进来JNIEnv
参数不会有错。但是当后台线程收到一个网络消息,又需要由Native
层函数回调Java
层函数时,JNI如何获取?我们不能保存另外一个线程的JNIEnv
结构体,然后把他们放到后台线程中来用。
在上面的JNI_OnLoad
函数中,第一个参数是JavaVM
对象,它是虚拟机在JNI层的代表。
//全进程只有一个JavaVM对象,所以可以保存,并且在任何地方使用都没有问题
jint JNI_OnLoad(JavaVM* vm, void* reserved);
在整个JNI_Onload
进程中,有且只有一个Java VM
对象,可以保存并在任何地方使用它,通过Java VM
对象,可以获取到JNIEnv
对象:
方法1:(*jvm)->AttachCurrentThread(jvm,(void**)&env,NULL);
方法2:(*jvm)->GetEnv(jvm,(void**)&env,JNI_VERSION_1_4);
2.2.4 JNIEnv 操作jobject对象
Java中的引用类型除了少数几个外,其余的用jobject
对象表示。操作jobject
对象的本质就应当是操作这些对象的成员变量和成员函数。
jfiledID
和jmethodID
我们可以通过jfiledID
和jmethodID
来表示Java
类的成员变量和成员函数,可通过JNIEnv
的下面两个函数得到:
jfieldID GetFieldID(jclass clazz,const char * name,const char * sig );
jmethodID GetFieldID(jclass clazz,const char * name,const char * sig);
其中,jclass
代表Java
类,name
表示成员变量的名称,sig
为这个函数和变量的签名信息。
#android_media_MediaScanner.cpp::MyMediaScannerClient类的构造函数 :
static const char* const kClassMediaScannerClient =
"android/media/MediaScannerClient";
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
ALOGV("MyMediaScannerClient constructor");
jclass
//先找到android.medai.MediaScannerClient类在JNI层中对应的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");
mSetMimeTypeMethodID = env->GetMethodID(
mediaScannerClientInterface,
"setMimeType",
"(Ljava/lang/String;)V");
}
}
如果每次操作jobject前都去查询jmethodID或jfieldID,那么将会影响程序运行的效率,所以我们在初始化的时候可以取出这些ID并保存起来以供后续使用。
2. 使用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)
{
ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
path, lastModified, fileSize, isDirectory);
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
/*
*调用JNIenv的`CallVoidMehod`函数,注意CallVoidMethod的参数 ;第一个是代表MediaScannerClient的jobject对象。第二个参数是函数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(JNIEnve * env, jobject obj,jmethodID methodID,... )
type对应Java函数的返回值类型。
上面是针对非static函数,如果想调用Java中的static函数,则用CallStatic<Type>Method
系列函数。
通过JNIEnv
操作jobject
的成员函数,通过jfieldID
来进行操作:
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)
void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)
2.2.5 jstring
介绍
Java中String类的存储类型为Unicode
字符。JNI中用jstring类型来 表示Java中的Stirng
下面是关于JNIEnv中关于Stirng
jstring (*NewString)(JNIEnv*, const jchar*, jsize);
jstring (*NewStringUTF)(JNIEnv*, const char*);
//得到本地`Unicode`字符
const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);
//得到本地`UTF-8`字符
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
//释放本地字符串
void (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);
void (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
jsize (*GetStringLength)(JNIEnv*, jstring);
jsize (*GetStringUTFLength)(JNIEnv*, jstring);
释放本地字符串示例:
static void
android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jobject client)
{
ALOGV("processDirectory");
MediaScanner *mp = getNativeScanner_l(env, thiz);
...
//调用JNIEnv 的GetStringUTFChars得到本地字符串pathStr
const char *pathStr = env->GetStringUTFChars(path, NULL);
...
//使用完后,必须调用`ReleaseStringUTFChars`释放资源
env->ReleaseStringUTFChars(path, pathStr);
}
2.2.6 JNI
类型签名介绍
Java中函数签名信息由参数类型和返回值类型共同组成。由于Java支持函数重载,函数名可以相同,而参数不同。
JNI中将参数类型和返回值类型的组合作为了一个函数的签名,来寻找函数的签名。
类型标志:
函数签名小例子: