JNI系列
JNI两种注册过程实战
深入理解JNI
最近在学习android底层的一些东西,看了一些大神的博客,整体上有了一点把握,也产生了很多疑惑,于是再次把邓大神的深入系列翻出来仔细看看,下面主要是一些阅读笔记。
JNI是Java Native Interface的缩写 ,通常称为“Java本地调用”,通过这种技术可以做到:
Java程序中的函数可以调用Native语言写的函数,Native一般是指C/C++编写的函数;
Native程序中的函数可以调用Java层的函数,也就是说C/C++程序可以调用Java函数。
通过JNI可以将底层Native世界和java世界联系起来
Java层对应的是MediaScanner,这个类有一些函数需要由Native层来实现
JNI层对饮libmedia_jni.so,一般采用
lib模块名_jni.so
的命名方式Native层对应的是libmedia.so,这个库完成了实际的功能
Java调用native函数,就需要通过一个位于JNI层的动态库来实现,这个通常是在类的static语句中加载,调用System.loadLibrary
方法,该方法的参数是动态库的名称,在这里为media_jni(系统会根据不同平台扩展成真实的动态库文件名,如在linux中libmedia_jni.so,而在windows平台则会扩展为media_jin.dll)
[MediaScanner.java]
`static {
//加载对应的JNI库media_jni是JNI库的名称。实际动态加载时将其扩展成为libmedia_jni.so
//在windows平台则扩展成为media_jni.dll
System.loadLibrary("media_jni");
native_init();//调用native_init函数
……
//申明一个native函数,表示它由JNI层完成
private native void processFile(String path, String mimeType, MediaScannerClient client);
……
private static native final void native_init();
}`
即java层的native_init和processFile[MediaScanner.java如上]函数对应的是JNI层的android_media_MediaScanner_native_init和android_media_MediaScanner_processFile[android_media_MediaScanner.cpp如下]函数呢?
`
//native_init的JNI层实现
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", "J");
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);
……
//调用JNIEnv的GetStringUTFChars得到本地字符串pathStr
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.
//使用完记得释放资源否则会引起JVM内存泄露
env->ReleaseStringUTFChars(path, pathStr);
return;
}
……
}
`
注册之意就是将Java层的native函数与JNI层对应的实现函数关联起来,这样在调用java层的native函数时,就能顺利转到JNI层对应的函数执行。
拿native_init来说,在android.media这个包中,全路径为andorid.media.MediaScanner.native_init
而JNI函数名字是android_media_MediaScanner_native_init
,由于在Native语言中符号“.”有着特殊意义需要将java函数名(包括包名)中的“.”换成“_”,这样java中的native_init找到JNI中的android_media_MediaScanner_native_init
注册的两种方式
静态方式
动态方式
静态方式
根据函数名来找对应的JNI函数,需要java的工具程序javah参与,流程如下:
先编写java代码,然后编译生成.class文件
使用java的工具程序javah,如javah -o output packagename.classname 这样就会生成一个叫output的JNI层头文件(函数名有_转换后为_l)
在静态方法中native函数是如何找到的,过程如下:当java层调用native_init函数时,它会从对应的JNI库中寻找java_android_media_MediaScanner_native_linit函数,如果没有找到,就会报错,如果找到就会为native_init和java_android_media_MediaScanner_native_linit建立一个函数指针,以后再调用native时直接使用这个指针即可,这个工作是由虚拟机完成
缺点:每个class都需要使用javah生成一个头文件,并且生成的名字很长书写不便;初次调用时需要依据名字搜索对应的JNI层函数来建立关联关系,会影响运行效率
动态注册
使用一种数据结构JNINativeMethod
来记录Java native函数和JNI函数的对应关系
`typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;`
[android_media_MediaScanner.cpp]中native_init和processFile的动态注册
`//动态注册
//定义一个JNINativeMethod数组,其成员就是MS中所有native函数一一对应关系
static JNINativeMethod gMethods[] = {
……
{
"processFile", //java中native函数的函数名
//processFile的签名信息
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(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)
{
return AndroidRuntime::registerNativeMethods(env,
kClassMediaScanner, gMethods, NELEM(gMethods));
}
`
这里使用AndroidRunTime类提供的registerNativeMethods将getMethods来完成注册工作
[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.c]
`
/*
* 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);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'\n", className);
return -1;
}
//实际上是调用了JNIEnv的RegisterNatives函数完成注册的
if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
LOGE("RegisterNatives failed for '%s'\n", className);
return -1;
}
return 0;
}
`
从这里我们可以清晰看出函数调用关系
`AndroidRuntime::registerNativeMethods
jniRegisterNativeMethods`
而在jniRegisterNativeMethods
中核心步骤只有两步
通过类名找到类(env指向一个JNIEnv结构体,className为对应Java类名,由于JNINativeMethod中使用的函数名并非全路径名,这里要指明具体类)
jclass clazz = (*env)->FindClass(env, className);
调用JNIEnv的RegisterNatives函数完成注册关联关系
(*env)->RegisterNatives(env, clazz, gMethods, numMethods)
何时调用该动态注册函数?
在第一小节调用native函数时首先使用System.loadLibrary
来加载动态库,当加载完成JNI动态库后,紧接着会查找该库汇总一个叫JNI_OnLoad
的函数,如果有就调用该函数,动态注册工作就是在这里完成。因此要实现动态注册就必须实现JNI_OnLoad函数,只有在这个函数中才有机会完成动态注册的工作。这里是放在了android_media_MediaPlayer.cpp中
[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) {
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;
}`
ok,至此JNI注册结束
在注册过程中JNIEnv已经多次出现,这里做下详细介绍。代表JNI环境的结构体
而且JNIEnv是一个线程相关的,也就是说线程A有个JNIEnv,线程B有个JNIEnv。由于线程相关不能在B线程中去访问线程A的JNIEnv结构体。由于我们无法保存一个线程的JNIEnv结构体,然后放到后台线程中去使用。为了解决这个问题,在
JNI_OnLoad函数中第一个参数是JavaVM对象,它是虚拟机在JNI层的代表
`
//全进程只有一个javavm对象,所以可以保存,并且在任何地方使用都没有问题
JNI_OnLoad(JavaVM* vm, void* reserved )`
其中
调用JavaVM的AttachCunrrentThread函数,就可以的得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数
在后台线程退出前,需要调用JavaVM的Detach的DetachCurrentThread函数来释放对应的资源
这样就是可以方便使用JNIEnv了。
在JNI中除了基本类型数组、Class、String和Throwable外其余所有Java对象的数据类型在JNI中都用jobject表示(数据类型下一节会介绍),因此JNIEnv如何操作jobject显得很重要。
首先要取得这些属性和方法。操作jobject的本质就是操作这些对象的成员变量和成员函数。在JNI中使用jfieldID和jmethodID来表示Java类的成员变量和成员函数
`jfieldID GetFieldID(jclass clazz,const char *name,const char *sig)
jmethodID GetMethod(jclass clazz,const char *name,const char *sig)
`
其中jclass表示java类,name表示成员变量/成员函数名称,sig表示变量/函数的签名信息,使用如下所示
[android_media_MediaScanner.cpp]
` mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface,
"scanFile",
"(Ljava/lang/String;JJZZ)V");`
这里所做就是将这些ID保存以便于后续使用,使得运行效率更高。
获取这些属性/方法ID后再看如何使用,如前面已经获取了mScanFileMethodID
,下面是使用
` mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
`
清清楚楚,使用JNIEnv输出CallVoidMethod,再把jobject、jMethodID和对应的参数传入,就可以调用java对象的函数了。这里是无返回值对象,实际上JNIEnv输出了一些列类似CallVoidMethod的函数,如CallIntMethod等,实际形式如下
`NativeType CallMethod(JNIEnv *env,jobject obj,jmethodID methodID,……)`
其中type对应java函数返回值,要是调用java中的static函数,则需要使用JNIEnv输出的CallStaticMethod系列
同理通过jfieldID操作jobject的成员变量
`NativeType GetField(JNIEnv *env,jobject obj,jfieldID fieldID)
NativeType SetField(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)`
类型
java数据类型分为基本数据类型和引用数据类型两种
先看基本数据类型
Java | Native | JNI层字长 |
---|---|---|
boolean | jboolean | 8位 |
byte | jbyte | 8位 |
char | jchar | 16位 |
short | jshort | 16位 |
int | jint | 32位 |
long | jlong | 64位 |
float | jfloat | 32位 |
double | jdouble | 64位 |
再看引用类型
Java引用类型 | Native类型 |
---|---|
All objects | jobject |
java.lang.Class | jclass |
java.lang.String | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
java.lang.Throwabe实例 | jthrowable |
签名
由于java支持函数重载,因此仅仅根据函数名是无法找到具体函数的,为解决这个问题,JNI技术中就将参数类型和返回值类型组合作为一个函数的签名,
如在[MedaiScanner.java]processFile函数定义
` private native void processFile(String path, String mimeType, MediaScannerClient client);`
对应的JNI函数签名是
(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V
其中,括号内是参数标识,最右边是返回值类型的标识,void类型标识是V,当参数类型是引用类型时其格式是”L包名”,包中的点换成/。
类型标识表
类型标识 | java类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L/java/lanaugeString | String |
[I | int[] |
[L/java/lang/object | Object[] |
函数签名手动写很容易出错,java提供了一个javap的工具可以帮助生成函数或变量的签名信息
JNI中提供三种类型的引用来解决垃圾回收问题
Local Reference:本地引用,一旦JNI层函数返回,这些jobject就可能被垃圾回收
Global Reference:全局引用,不主动释放,永远不会被回收
Weak Global Reference:弱全局引用,在运行过程中可能会被垃圾回收,因此在使用之前,需要调用JNIEnv的isSameObject判断是否被回收
通过阅读本章主要学习了
JNI作用
结合MediaScanner等源码学习了JNI注册调用等过程
JNIEnv的用法
JNI中签名、数据类型、垃圾回收机制