一:JNI 概述
JNI全称为Java Native Interface,它能够使在Java虚拟机中运行的Java代码去操作用其他异于Java的编程语言(如C/C++)所编写的程序和库。通俗地说,JNI是一种技术,通过这种技术可以做到如下两点:
1. Java 程序中的函数可以调用Native 语言写的函数,Native一般指C/C++编写的函数。
2. Native程序中的函数可以调用java层的函数,也就是说在C/C++程序中可以调用Java 函数。
在Android 平台上,JNI就是一座将Native 世界和java世界之间通途的桥,并在大量使用,如surface、mediaPlayer 等等。看图1-1展示了Android 平台上JNI所处的位置:
图1-1 Android 平台中JNI的示意图
由上图可知,JNI将java与Native空间紧密联系在一起了。在很多linux版本应用移植到Android 平台上也可能使用JNI,如一下视频播放器可以通过JNI无缝过渡到Android平台上来。
二:JNI 函数注册
在介绍JNI函数注册前,先介绍两个JNI使用中重要的概念:JavaVM与JNIEnv。JavaVM与JNIEnv 本质上都是指向函数表的指针的指针,在C++版本中,它们被定义成类,类里面包含一个指向函数表的指针。具体参见头文件定义dalvik\libnativehelper\include\nativehelper\jni.h。JNIEnv是一个与线程相关的变量,不同线程的JNIEnv彼此独立。JavaVM是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此该进程的所有线程都可以使用这个JavaVM。
JNI方法是Android应用程序与本地操作系统直接进行通信的一个手段,这就需要将JAVA与C/C++函数注册到JNIEnv中,从下面代码段可以看出注册过程。
Java 程序:
package com.example.test.app;
public class MiddleWare {
... ...
static {
System.loadLibrary("testmw_jni");
nativeinit();
}
... ...
public native void openUrl(String urlStr);
... ...
public void browserUrl(String url) {
if(StringUtils.isEmpty(url)){
return;
}
openUrl(url);
}
......
}
C++ 程序:
static void _MiddlewareJNI_Native_Init(JNIEnv *env)
{
if (pthread_key_create(&g_EnvKey, _JNI_ThreadDestructor))
return ;
pthread_setspecific(g_EnvKey, env);
jclass clazz;
if (!(clazz = env->FindClass(TEST_CLASS_PATH_NAME)))
return ;
g_InstanceID = env->GetFieldID(clazz, "mNativeContext", "I");
}
static void _MiddlewareJNI_openUrl(JNIEnv *env, jobject thiz, jstring jUrl)
{
MiddlewareJNI* mv = (MiddlewareJNI*)env->GetIntField(thiz, g_InstanceID);
if (mv) {
const char* url = env->GetStringUTFChars(jUrl, 0);
if (url) {
mv->openUrl(url);
env->ReleaseStringUTFChars(jUrl, url);
}
}
}
static JNINativeMethod sMethods[] =
{
{"nativeinit", "()V", (void*)_MiddlewareJNI_Native_Init},
{"openUrl", "(Ljava/lang/String;)V", (void*)_MiddlewareJNI_openUrl}
};
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
jclass clazz = NULL;
JNIEnv* env = NULL;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_4) != JNI_OK)
return JNI_ERR;
if (!(clazz = env->FindClass(TEST_CLASS_PATH_NAME)))
return JNI_ERR;
env->RegisterNatives(clazz, sMethods, sizeof(sMethods) / sizeof(sMethods[0]));
g_JavaVM = vm;
return JNI_VERSION_1_4;
}
涉及到一些项目信息,为不体现具体项目内容,本文的例子都做过相关修改。以上代码段是测试实现的例子,Test应用实现成一个testmw_jni 的so文件中。因此,在该JNI方法能够被调用之前,我们首先要将它加载到当前应用程序进程来,这是通过调用System类的静态成员函数loadLibrary来实现的。当libtestmw_jni .so文件被加载的时候,函数JNI_OnLoad就会被调用。在函数JNI_OnLoad中,参数vm描述的是当前进程中的Dalvik虚拟机,通过调用它的成员函数GetEnv就可以获得一个JNIEnv对象。有了这JNIEnv对象之后,我们就可以调用另外一个函数RegisterNatives来向当前进程中的Dalvik虚拟机注册JNI方法,如上面代码段描述的nativeinit。注册过程,将java层的nativeinit() 函数与C/C++ 中的_MiddlewareJNI_Native_Init函数对应起来了。
从上述代码段中注册过程可以看出,JAVA层的函数前带有Java 关键字native,它表示对应函数将由JNI层来实现。在JNI层中与之对应的函数都使用static函数。
2.1 JNI数据类型转换规则
在java调用Native函数传递的参数是java数据类型,那么这些参数类型到了JNI会如何对应需要遵守JNI数据类型转换规则。Java 数据类型分为基本数据类型和引用数据类型两种,JNI层也是区分对待这二者的。详见表2-1 及表2-2。
表2-1,基本数据类型的转换关系表
JAVA |
Native 类型 |
符号属性 |
字长 |
boolean |
jboolean |
无符号 |
8位 |
byte |
jbyte |
无符号 |
8位 |
char |
jchar |
无符号 |
16位 |
short |
jshort |
有符号 |
16位 |
int |
jint |
有符号 |
32位 |
long |
jlong |
有符号 |
64位 |
float |
jfloat |
有符号 |
32位 |
double |
jdouble |
有符号 |
64位 |
表2-2 Java 引用数据类型转换表
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.Trowable实例 |
jthrowable |
|
|
2.2 JNI命名规则与签名规则
JNI实现方法与Java声明方法是不同的,例如:Java层声明的Native方法名是openUrl,而其对应的JNI实现方法的方法名却是_MiddlewareJNI_openUrl。可见,除了数据类型有对应关系外,方法名也有对应关系。JNI 接口指针是JNI实现方法的第一个参数,其类型是JNIEnv。第二个参数因本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对Java对象的引用,而静态本地方法的第二个参数是对其 Java 类的引用,其余的参数都对应于Java 方法的参数。JNI规范里提供了JNI实现方法的命名规则,方法名由以下几部分串接而成:
Java_前缀
全限定的类名
下划线(_)分隔符
增加第一参数JNIEnv* env
增加第二个参数jobject
其他参数按类型映射
返回值按类型映射
再看一段注册代码:
static JNINativeMethod sMethods[] =
{
......
{"openUrl", "(Ljava/lang/String;)V", (void*)_MiddlewareJNI_openUrl},
......
}
其中"(Ljava/lang/String;)V"字符是签名信息,有参数类型和返回值共同组成。之所以要签名是因为Java支持函数重载,可以定义相同的函数,不同的参数。仅仅通过函数名没有办法找到具体函数,为了解决这一问题,JNI技术将参数类型和返回值类型组合成一个函数的签名信息,有了签名信息和函数名,很顺利能找到java中的函数了。
JNI规范定义的签名格式如下:
(参数1类型标示参数2类型标示... 参数n类型标示)返回值类型标示
常见的类型标示示意如表2-3,java还存在其他签名的方式,如函数签名,这里不一一举例。
表2-3 类型标识示意图
类型标示 |
Java类型 |
类型标示 |
Java类型 |
Z |
boolean |
F |
float |
B |
byte |
D |
double |
C |
char |
L/java/langaugeString |
String |
S |
short |
[I |
int[] |
I |
int |
[L/java/lang/object |
Object[] |
J |
long |
|
|
2.3. 正向和反向通信
JNI是连通java层与C/C++层的桥梁,存在双向通信,正向java层调用C/C++ 层,反向C/C++调用java层。 Java 调用native 方法比较简单,framework中也有不少参考代码。 上述代码段也给出了示例,Java层调用native方法openUrl,JVM将会调用到native的_MiddlewareJNI_openUrl方法,其中在native method中,JNIEnv作为第一个参数传入,通过JNIEnv获取相应对象来处理相关实现。
反向通信是很容易使用错误,JNI开发最常见的错误就是滥用了JNIEnv接口。需要强调的是JNIEnv是跟线程相关的,而JavaVM是进程相关。要用的时候在不同线程中再通过JavaVM *jvm的方法来获取与当前线程相关的JNIEnv*。
正如前面所述,不论进程中有多少线程,JavaVM进程只有一份,JNIEnv与JavaVM关系如下:
1.调用JavaVM的AttachCurrentThread函数,就可以得到这个线程的JNIEnv结构体,这样就可以在后台线程中回调java函数。
2.在后台线程退出前,需要调用JavaVM 的DetachCurrentThread 函数来释放对应的资源。