由前面基础知识可知,Android的应用层由Java语言编写,Framework框架层则是由Java代码与C/C++语言实现,之所以由两种不同的语言组合开发框架层,是因为Java代码是与硬件环境彻底“隔离”的跨平台语言,Java代码无法直接操作硬件。比如:Android系统支持大量传感器,Java运行在虚拟机中,无法直接得到传感器数据,而Android系统基于Linux操作系统,在Linux操作系统中C/C++通过Linux提供的系统调用接口可以直接访问传感器硬件驱动,Java代码可以将自己的请求,交给底层的本地C/C++代码实现间接的对传感器的访问。另外,Java代码的执行效率要比C/C++执行效率要低,在一些对性能要求比较高的场合,也要使用C/C++来实现程序逻辑。
既然Java代码要请求本地C/C++代码,那么二者必须要通过一种媒介或桥梁联系起来,这种媒介就是Java本地接口(Java NativeInterface),它是Java语言支持的一种本地语言访问方式,JNI提供了一系列接口,它允许Java与C/C++语言之间通过特定的方式相互调用、参数传递等交互操作。
通常在以下几种情况下考虑使用JNI:
Ø 对处理速度有要求
Java代码执行速度要比本地代码(C/C++)执行速度慢一些,如果对程序的执行速度有较高的要求,可以考虑使用C/C++编写代码,然后在通过Java代码调用基于C/C++编写的部分。
Ø 硬件控制
如前面所述,Java运行在虚拟机中,和真实运行的物理硬件之间是相互隔离的,通常我们使用本地代码C实现对硬件驱动的控制,然后再通过Java代码调用本地硬件控制代码。
Ø 复用本地代码
如果程序的处理逻辑已经由本地代码实现并封装成了库,就没有必要再重新使用Java代码实现一次,直接复用该本地代码,即提高了编程效率,又确保了程序的安全性和健壮性。
在计算机中,每种编程语言都有一个执行环境(Runtime),执行环境用来解释执行语言中的语句,不同的编程语言的执行环境就好比如西游记中的“阴阳两界”一样,一般人不能同时生存在阴阳两界中,只有“黑白无常”能自由穿梭在阴阳两界之间,“黑白无常”往返于阴阳两界时手持生死簿,“黑白无常”按生死簿上记录着的人名“索魂”。
Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机进程在本地环境中都有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI中创建JVM的函数为JNI_CreateJavaVM。
JNI_CreateJavaVM(JavaVM **pvm, void **penv, void*args);
JavaVM结构中封装了一些函数指针(或叫函数表结构),JavaVM中封装的这些函数指针主要是对JVM操作接口,如下面代码所示:
@jni.h
struct JNIInvokeInterface_ {
void *reserved0;
void *reserved1;
void *reserved2;
jint (JNICALL *DestroyJavaVM)(JavaVM *vm); // 销毁Java虚拟机并回收资源,只有JVM主线程可以销毁
jint (JNICALL *AttachCurrentThread)(JavaVM *vm,void **penv, void *args); // 连接当前线程为Java线程
jint (JNICALL *DetachCurrentThread)(JavaVM*vm); // 释放当前Java线程
jint (JNICALL *GetEnv)(JavaVM *vm, void**penv, jint version); // 获得当前线程的Java运行环境
jint (JNICALL*AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);// 连接当前线程作为守护线程
};
struct JavaVM_ {
const struct JNIInvokeInterface_*functions;
jint DestroyJavaVM() {
returnfunctions->DestroyJavaVM(this);
}
…省略部分代码
jint GetEnv(void **penv, jintversion) {
returnfunctions->GetEnv(this, penv, version);
}
…省略部分代码
};
#ifdef __cplusplus
typedef JavaVM_ JavaVM;
#else
typedef const structJNIInvokeInterface_ *JavaVM;
#endif
通过上面代码分析可知,JNIInvokeInterface_结构封装了几个和JVM相关的功能函数,如销毁JVM,获得当前线程的Java执行环境。另外,在C和C++中JavaVM的定义有所不同,在C中JavaVM是JNIInvokeInterface_类型指针,而在C++中又对JNIInvokeInterface_进行了一次封装,比C中少了一个参数,这也是为什么JNI代码更推荐用C++来编写的原因。
JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储(TLS)中。因此,不同的线程的JNIEnv是不同,也不能相互共享使用。
JNIEnv结构也是一个函数表,在本地代码中通过JNIEnv的函数表来操作Java数据或调用Java方法。也就是说,只要在本地代码中拿到了JNIEnv结构,就可以在本地代码中调用Java代码。
@jni.h中对JNIEnv的定义如下:
struct JNINativeInterface_ {
…
jclass (JNICALL*FindClass) (JNIEnv *env, const char *name);
…定义大量JNI函数指针
};
struct JNIEnv_ {
const structJNINativeInterface_ *functions;
jclass FindClass(constchar *name) {
return functions->FindClass(this, name); //调用JNINativeInterface_中的函数指针
}
…省略部分代码
};
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const structJNINativeInterface_ *JNIEnv;
#endif
由上面代码可知,和JavaVM类似,JNIEnv在C代码和C++代码中的使用方式也是不一样的,在C++中对JNINativeInterface_结构又进行了一次封装,调用起来更方便。
总体来说,JNI其实就是定义了Java语言与本地语言间的一种沟通方式,这种沟通方式依赖于JavaVM和JNIEnv结构中定义的函数表,这些函数表负责将Java中的方法调用转换成对本地语言的函数调用。
当Java代码与本地C\C++代码相互调用时,肯定会有参数数据的传递。两者属于不同的编程语言,在数据类型上有很多差别。JNI要保证它们两者之间的数据类型和数据空间大小的匹配。尽管C和Java中都拥有int和char的数据类型,但是他们的长度却不尽相同。在C语言中,int类型的长度取决与平台,char类型为1个字节,而在Java语言中,int类型恒为4字节,char类型为2字节。为了使Java语言和本地语言类型、长度匹配,JNI中定义了jint,jchar等类型,在JNI中定义了一些新的数据类型,如下表所示。
表xx-xx
Java Language Type |
JNI Type |
boolean |
jboolean |
byte |
jbyte |
char |
jchar |
short |
jshort |
int |
jint |
long |
jlong |
float |
jfloat |
double |
jdouble |
All Reference type |
jobject |
由Java类型和JNI数据类型的对应关系可以看到,这些新定义的JNI类型名称和Java类型名称具有一致性,只是在前面加了个j,如int对应jint,long对应jlong。我们可以通过JDK目录中的jni.h和jni_md.h来更直观的了解:
@ jni_md.h
…省略部分代码
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
…省略部分代码
@jni.h
// JNI类型与C/C++类型对应关系声明
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;
由jni头文件可以看出,jint对应的是C/C++中的long类型,即32位整数,而不是C中的int类型(C中的int类型长度依赖于平台)。所以如果要在本地方法中要定义一个jint类型的数据,规范的写法应该是 jinti=123L;
再比如jchar代表的是Java类型的char类型,实际上在C/C++中却是unsigned short类型,因为Java中的char类型为两个字节,jchar相当于C/C++中的宽字符。所以如果要在本地方法中要定义一个jchar类型的数据,规范的写法应该是jcharc=L'C';
实际上,所有带j的类型,都是JNI对应的Java的类型,并且jni中的类型接口与本地代码在类型的空间大小是完全匹配的,而在语言层次却不一定相同。在本地方法中与JNI接口调用时,要在内部都要转换,我们在使用的时候也需要小心。
在本地代码中为了访问Java运行环境中的引用类型,在JNI中也定义了一套对应的引用类型,它们的对应关系如下:
JNI引用类型 |
Java引用类型 |
jobject |
所有引用类型父类Object |
jclass |
java.lang.Class类型 |
jstring |
java.lang.String类型 |
jarray |
数组类型 |
jobjectArray |
对象数组类型 |
jbooleanArray |
布尔数组类型 |
jbyteArray |
字节数组类型 |
jcharArray |
字符数组类型 |
jshortArray |
短整形数组类型 |
jintArray |
整形数组类型 |
jlongArray |
长整形数组类型 |
jfloatArray |
浮点数组类型 |
jdoubleArray |
双精度数组类型 |
jthrowable |
java.lang.Throwadble类型 |
由上表内容可知,JNI引用类型都是以j开头类型。与Java中所有类的父类为Object一样,所有的JNI中的引用类型都是jobject的子类,JNI这些j类型和Java中的类一一对应,只不过名字稍有不同而已。
由4.1节可知,在某些情况下一些功能由本地代码来实现,这时Java代码需要调用这些本地代码,在调用本地代码时,首先要保证本地代码被加载到Java执行环境中并与Java代码链接在一起,这样当Java代码在调用本地方法时能保证找到并调用到正确的本地代码,然后在Java中要显示声明本地方法为native方法。其实现步骤如下:
· 编写Java代码,在Java代码中加载本地代码库
· 在Java中声明本地native方法
· 调用本地native方法
例如:
public class HelloJNI{
static{
System.loadLibrary(“hellojni”); // 通过System.loadLibrary()来加载本地代码库
}
privatestatic native String getJNIHello(); // 由于该方法的实现在本地代码中,所以加上native关键字进行声明
publicstatic void main(String args[]){
System.out.println(HelloJNI.getJNIHello()); // 调用本地方法
}
}
上述代码的执行需要本地代码库hellojni,正常运行的话会在屏幕上打印:Helloworld字符串。由代码可知,在Java中调用本地代码不是很复杂,本地代码库的加载系统方法System.loadLibrary在static静态代码块中实现的,这是因为静态代码块只会在Java类加载时被调用,并且只会被调用一次。本地代码库的名字为hellojni,如果在Windows中则其对应的文件名为:hellojni.dll,如果在Linux中,其对应的文件名为libhellojni.so。
思考:
1. 可不可以将本地代码库的加载放到构造方法中?放在非静态代码块中呢?
2. native方法可以声明为abstract类型的吗?
Native关键字本身和abstract关键字冲突,他们都是方法的声明,只是一个是把方法实现移交给子类,另一个是移交给本地代码库。如果同时出现,就相当于即把实现移交给子类,又把实现移交给本地操作系统,那到底谁来实现具体方法呢?
在JNI调用中,不仅仅Java可以调用本地方法,本地代码也可以调用Java中的方法和成员变量。在Java1.0中规定:Java和C本地代码绑定后,程序员可以直接访问Java对象数据域。这就要求虚拟机暴露它们之间内部数据的绑定关系,基于这个原因,JNI要求程序员通过特殊的JNI函数来获取和设置数据以及调用java方法。
Java中的类封装了属性和方法,要想访问Java中的属性和方法,首先要获得Java类或Java对象,然后再访问属性、调用方法。
在Java中类成员指静态属性和静态方法,它们属于类而不属于对象。而对象成员是指非静态属性和非静态方法,它们属于具体一个对象,不同的对象其成员是不同的。正因为如此,在本地代码中,对类成员的访问和对对象成员的访问是不同的。
在JNI中通过下面的函数来获得Java运行环境中的类。
jclass FindClass(const char *name);
name:类全名,包含包名,其实包名间隔符用“/”代替“.”
例如:
jclass jActivity = env->FindClass(“java/lang/String”);
上述JNI代码获得Android中的Activity类保存在jActivity中。
在JNI中Java对象一般都是作为参数传递给本地方法的。
例如:
Java代码:
package com.test.exam1;
class MyClass{
privateint mNumber;
privatestatic String mName;
publicMyClass(){
}
publicvoid printNum(){
System.out.println(“Number:” + mNumber);
}
publicstatic void printNm(){
System.out.println(“Number:” + mNumber);
}
}
class PassJavaObj{
static{
System.loadLibrary(“native_method”);
}
privatenative static void passObj(String str);
publicstatic void main(String arg[]){
passObj(“HelloWorld”);
}
}
本地代码:
void Java_com_test_exam1_PassJavaObj_passObj
(JNIEnv * env, jclassthiz, jobject str)
{
…
}
在上述例子中,Java代码中将“Hello World”字符串对象传递给了本地代码,在本地代码对应的方法中,共有三个参数,其中前两个参数是由Java运行环境自动传递过来的,env表示当前Java代码的运行环境,thiz表示调用当前本地方法的对象,这两个参数在每个本地方法中都有。第三个参数str就是我们传递过来的字符串。
在本地方法中拿到了类或对象后,JNI要求程序员通过特殊的JNI函数来获取和设置Java属性以及调用java方法。
为了在C/C++中表示Java的属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID类型来分别代表Java对象的属性和方法。我们在访问或是设置Java属性的时候,首先就要先在本地代码取得代表该Java属性的jfieldID,然后才能对本地代码的Java属性进行操作。同样的,我们需要调用Java方法时,也需要取得代表该方法的jmethodID才能进行Java方法调用。
使用JNIEnv提供的JNI方法,我们就可以获得属性和方法相对应的jfieldID和jmethodID:
@jni.h
// 根据属性签名返回 clazz类中的该属性ID
jfieldID GetFieldID(jclass clazz, const char*name, const char *sig);
// 如果获得静态属性ID,则调用下面的函数
jfieldID GetStaticFieldID(jclass clazz, constchar *name, const char *sig);
// 根据方法签名返回clazz类中该方法ID
jmethodID GetMethodID(jclass clazz, const char*name, const char *sig);
// 如果是静态方法,则调用下面的函数实现
jmethodID GetStaticMethodID(jclass clazz, constchar *name, const char *sig);
可以看到这四个方法的参数列表都是一模一样的,下面来分析下每个参数的含义:
· jclass clazz:要取得成员对应的类
· const char *name:代表我们要取得的方法名或者属性名
· const char *sig:代表我们要取得的方法或属性的签名
我们将一个例子进行简单修改:
package com.test.exam2;
class MyClass{
privateint mNumber;
privatestatic String mName = “Michael”;
publicMyClass(){
mNumber= 1000;
}
publicvoid printNum(){
System.out.println(“Number:” + mNumber);
}
publicstatic void printNm(){
System.out.println(“Number:” + mNumber);
}
}
class NativeCallJava{
static{
System.loadLibrary(“native_callback”);
}
privatenative static void callNative(MyClass cls);
publicstatic void main(String arg[]){
callNative(newMyClass());
}
}
本地代码:
void Java_com_test_exam2_NativeCallJava_callNative(JNIEnv* env, jclassthiz, jobject obj)
{
jclass myCls = env->GetObjectClass(obj);
jfieldIDmNumFieldID = env->GetFieldID(myCls, “mNumber”, “I”);
jfieldIDmNameFieldID = env->GetStaticFieldID(myCls, “mName”, “java/lang/String”);
jmethodIDprintNumMethodID = env->GetMethodID(myCls, “printNum”, “(V)V”);
jmethodIDprintNmMethodID = env->GetStaticMethodID(myCls, “printNm” , “(V)V”);
}
在上述Java代码中,我们自己定义了一个类MyClass,里面定义了对应的静态和非静态成员,然后将MyClass对象传递给本地代码。在本地代码中通过GetObjectClass方法取得MyClass对象对应的类,然后依次取得MyClass类中的属性ID和方法ID。
其中GetObjectClass定义如下:
jclass GetObjectClass(jobject obj) ;
jobject obj:如果本地代码拿到一个对象,则通过该方法取得该对象的类类型,其功能如同Object.getClass()方法。
Java语言是面向对象的语言,它支持重载机制,即:允许多个具有相同的方法名不同的方法签名的方法存在。因此,只通过方法名不能明确的让JNI找到Java里对应的方法,还需要指定方法的签名,即:参数列表和返回值类型。
JNI中类型签名如下表所示:
类型签名 |
Java类型 |
类型签名 |
Java类型 |
Z |
boolean |
[ |
[] |
B |
byte |
[I |
int[] |
C |
char |
[F |
float[] |
S |
short |
[B |
byte[] |
I |
int |
[C |
char[] |
J |
long |
[S |
short[] |
F |
float |
[D |
double[] |
D |
double |
[J |
long[] |
L |
类 |
[Z |
boolean[] |
V |
void |
|
|
· 基本类型
以特定的单个大写字母表示
· Java类类型
Java类类型以L开头,以“/”分隔包名,在类名后加上“;”分隔符,例如String的签名为:Ljava/lang/String;
在Java中数组是引用类型,数组以“[”开头,后面跟数组元素类型签名,例如:int[]的签名是[I ,对于二维数组,如int[][]签名就是[[I,object数组签名就是[Ljava/lang/Object;
对于方法签名,在JNI中也有特定的表示方式。
(参数1类型签名参数2类型签名参数3类型签名.......)返回值类型签名
注意:
· 方法名在方法签名中没有体现出来
· 括号内表示参数列表,参数列表紧密相挨,中间没有逗号,没有空格
· 返回值出现在括号后面
· 如果函数没有返回值,也要加上V类型
例如:
Java方法 |
JNI方法签名 |
boolean isLedOn(void) ; |
(V)Z |
void setLedOn(int ledNo); |
(I)V |
String substr(String str, int idx, int count); |
(Ljava/lang/String;II)Ljava/lang/String |
char fun (int n, String s, int[] value); |
(ILjava/lang/String;[I)C |
boolean showMsg(android.View v, String msg); |
(Landroid/View;Ljava/lang/String;)Z |
1. 获得、设置属性和静态属性
取得了代表属性和静态属性的jfieldID,就可以使用JNIEnv中提供的方法来获取和设置属性/静态属性。
取得Java属性的JNI方法定义如下:
j<类型> Get<类型>Field(jobjectobj, jfieldID fieldID);
j<类型> Get Static<类型>Field(jobjectobj, jfieldID fieldID);
设置Java属性的JNI方法定义如下:
void Set<类型>Field(jobject obj, jfieldID fieldID, j<类型> val);
void Set Static<类型>Field(jobject obj, jfieldID fieldID, j<类型> val);
<类型>表示Java中的基本类型。例如:
// 获得obj对象中,整形属性ID为fieldID的值
jint GetIntField(jobject obj, jfieldID fieldID);
// 设置obj对象中,属性ID为fieldID,属性值为val
void SetObjectField(jobjectobj, jfieldID fielded,jobject val);
// 设置clazz类中,静态属性ID为fieldID的属性值为value
void SetStaticCharField(jclass clazz, jfieldIDfieldID, jchar value)
我们将上一节中的本地代码进行修改如下:
void Java_com_test_exam2_NativeCallJava_callNative(JNIEnv* env, jclassthiz, jobject obj)
{
jclass myCls = env->GetObjectClass(obj);
jfieldIDmNumFieldID = env->GetFieldID(myCls, “mNumber”, “I”);
jfieldIDmNameFieldID = env->GetStaticFieldID(myCls, “mName”, “java/lang/String”);
//获得、设置Java成员的属性值
jintmNum = env->GetIntField(obj, mNumFieldID);
env->SetIntField(obj,mNumFieldID, mNum+100);
//获得、设置静态属性的值
jstringmNm = (jstring)(env->GetStaticObjectField(myCls, mNameFieldID));
printf(“%s\n”,mNm);
jstringnewStr = env->NewStringUTF(“Hello from Native”);
env->SetStaticObjectField(myCls,mNumFieldID, newStr);
…
}
上述代码通过JNI提供的Get、Set方法取得和设置Java对象和类的属性,NewStringUTF表示创建一个Java的字符串对象,字符串值使用8位字符初始化。
2. 通过JNI调用Java中的方法
在4.5.1节我们通过JNI方法获得了jmethodID,在本地代码中我们就可以通过jmethodID来调用Java中的方法了。
JNI提供了下面的方法用来调用Java方法:
// 调用Java成员方法
Call<Type>Method(jobject obj,jmethodIDmethodID,...);
Call<Type>MethodV(jobject clazz, jmethodIDmethodID,va_listargs);
Call<Type>tMethodA(jobject clazz,jmethodID methodID,constjvalue *args);
// 调用Java静态方法
CallStatic<Type>Method(jclass clazz,jmethodID methodID,...);
CallStatic<Type>MethodV(jclass clazz,jmethodID methodID,va_listargs);
CallStatic<Type>tMethodA(jclass clazz,jmethodID methodID,constjvalue *args);
上面的Type这个方法的返回值类型,如void,int,char,byte等等。
第一个参数代表调用的这个方法所属于的对象,或者这个静态方法所属的类。
第二个参数代表jmethodID。
后面的表示调用方法的参数列表,…表示是变长参数,以“V”结束的方法名表示以向量表形式提供参数列表,以“A”结束的方法名表示以jvalue数组提供参数列表,这两种调用方式使用较少。
我们将前面的例子的本地代码继续进行修改:
void Java_com_test_exam2_NativeCallJava_callNative(JNIEnv* env, jclassthiz, jobject obj)
{
jclass myCls = env->GetObjectClass(obj);
jfieldIDmNumFieldID = env->GetFieldID(myCls, “mNumber”, “I”);
jfieldIDmNameFieldID = env->GetStaticFieldID(myCls, “mName”, “java/lang/String”);
//获得、设置Java成员的属性值
jintmNum = env->GetIntField(obj, mNumFieldID);
env->SetIntField(obj,mNumFieldID, mNum+100);
//获得、设置静态属性的值
jstringmNm = (jstring)(env->GetStaticObjectField(myCls, mNameFieldID));
printf(“%s\n”,mNm);
jstringnewStr = env->NewStringUTF(“Hello from Native”);
env->SetStaticObjectField(myCls,mNumFieldID, newStr);
//取得Java方法ID
jmethodIDprintNumMethodID = env->GetMethodID(myCls, “printNum”, “(V)V”);
jmethodIDprintNmMethodID = env->GetStaticMethodID(myCls, “printNm” , “(V)V”);
//调用MyClass对象中的printNum方法
CallVoidMethod(obj,printNumMethodID);
//调用MyClass类的静态pinrtNm方法
CallStaticVoidMethod(myCls,printNmMethodID);
}
在Java中构造方法是一种特殊的方法,主要用于对象创建时被回调,我们将在下一节分析。
1. 在本地代码中创建Java对象
在JNIEnv的函数表中提供了下面几个方法来创建一个Java对象:
jobject NewObject(jclass clazz, jmethodIDmethodID,...);
jobject NewObjectV(jclass clazz,jmethodIDmethodID,va_list args);
jobjectNewObjectA(jclass clazz, jmethodIDmethodID,const jvalue *args) ;
它们和上一节中介绍的调用Java方法使用起来很相似,他们的参数意义如下:
clazz:要创建的对象的类。
jmethodID:创建对象对应的构造方法ID。
参数列表:…表示是变长参数,以“V”结束的方法名表示向量表表示参数列表,以“A”结束的方法名表示以jvalue数组提供参数列表。
由于Java的构造方法的特点是方法名与类名一样,并且没有返回值,所以对于获得构造方法的ID的方法env->GetMethodID(clazz,method_name,sig)中的第二个参数是固定为类名(也可以用“<init>”代替类名),第三个参数和要调用的构造方法有关,默认的Java构造方法没有返回值,没有参数。
我们将上一节的例子进行修改,在本地代码中创建一个新的MyClass对象:
void Java_com_test_exam2_NativeCallJava_callNative(JNIEnv* env, jclassthiz, jobject obj)
{
jclass myCls = env->GetObjectClass(obj);
//当然我们也可以像下面这样写:
//jclass myCls = env->FindClass("com/test/exam2/MyClass");
//取得MyClass的构造方法ID
jmethodIDmyClassMethodID = env->GetMethodID(myCls, “MyClass”, “(V)V”);
//创建MyClass新对象
jobjectnewObj = NewObject(myCls, myClassMethodID);
}
2.创建Java String对象
在Java中,字符串String对象是Unicoode(UTF-16)编码,每个字符不论是中文还是英文还是符号,一个字符总是占用两个字节。在C/C++中一个字符是一个字节, C/C++中的宽字符是两个字节的。
在本地C/C++代码中我们可以通过一个宽字符串,或是一个UTF-8编码的字符串创建一个Java端的String对象。这种情况通常用于返回Java环境一个String返回值等场合。
根据传入的宽字符串创建一个Java String对象
jstring NewString(const jchar *unicode, jsizelen)
根据传入的UTF-8字符串创建一个Java String对象
jstring NewStringUTF(const char *utf)
在Java中String类有很多对字符串进行操作的方法,在本地代码中可以通过JNI接口可以将Java的字符串转换到C/C++的宽字符串(wchar_t*),或是传回一个UTF-8的字符串(char*)到C/C++,在本地进行操作。
可以看下面的一个例子:
在Java端有一个字符串 String str="abcde";,在本地方法中取得它并且输出:
void native_string_operation (JNIEnv * env, jobject obj)
{
//取得该字符串的jfieldID
jfieldIDid_string=env->GetFieldID(env->GetObjectClass(obj),"str", "Ljava/lang/String;");
jstringstring=(jstring)(env->GetObjectField(obj, id_string)); //取得该字符串,强转为jstring类型。
printf("%s",string);
}
由上面的代码可知,从java端取得的String属性或者是方法返回值的String对象,对应在JNI中都是jstring类型,它并不是C/C++中的字符串。所以,我们需要对取得的 jstring类型的字符串进行一系列的转换,才能使用。
JNIEnv提供了一系列的方法来操作字符串:
将一个jstring对象,转换为(UTF-16)编码的宽字符串(jchar*):
const jchar *GetStringChars(jstring str,jboolean*isCopy)
将一个jstring对象,转换为(UTF-8)编码的字符串(char*):
const char *GetStringUTFChars(jstringstr,jboolean *isCopy)
这两个函数的参数中,第一个参数传入一个指向Java 中String对象的jstring引用。第二个参数传入的是一个jboolean的指针,其值可以为NULL、JNI_TRUE、JNI_FLASE。
如果为JNI_TRUE则表示在本地开辟内存,然后把Java中的String拷贝到这个内存中,然后返回指向这个内存地址的指针。如果为JNI_FALSE,则直接返回指向Java中String的内存指针。这时不要改变这个内存中的内容,这将破坏String在Java中始终是常量的规则。
如果是NULL,则表示不关心是否拷贝字符串。
使用这两个函数取得的字符,在不适用的时候(不管String的数据是否拷贝到本地内存),要分别对应的使用下面两个函数来释放内存。
RealeaseStringChars(jstring jstr,const jchar*str)
RealeaseStringUTFChars(jstringjstr, constchar* str)
第一个参数指定一个jstring变量,即要释放的本地字符串的资源
第二个参数就是要释放的本地字符串
我们可以使用GetFieldID获取一个Java数组变量的ID,然后用GetObjectFiled取得该数组变量到本地方法,返回值为jobject,然后我们可以强制转换为j<Type>Array类型。
@jni.h
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;
j<Type>Array类型是JNI定义的一个对象类型,它并不是C/C++的数组,如int[]数组,double[]数组等等。所以我们要把j<Type>Array类型转换为C/C++中的数组来操作。
JNIEnv定义了一系列的方法来把一个j<Type>Array类型转换为C/C++数组或把C/C++数组转换为j<Type>Array。
1. 获取数组的长度
jsize GetArrayLength(jarray array);
2. 对象类型数组的操作
jobjectArray NewObjectArray(jsize len, jclassclazz, jobject init) // 创建对象数组
jobject GetObjectArrayElement(jobjectArrayarray, jsize index) // 获得元素
void SetObjectArrayElement(jobjectArray array,jsize index, jobject val) // 设置元素
参数说明:
len:新创建对象数组长度
clazz:对象数组元素类型
init:对象数组元素的初始值
array:要操作的数组
index:要操作数组元素的下标索引
val:要设置的数组元素的值
JNI没有提供直接把Java的对象类型数组(Object[ ])直接转到C++中的jobject[ ]数组的函数。而是直接通过Get/SetObjectArrayElement这样的函数来对Java的Object[ ]数组进行操作。
3. 对基本数据类型数组的操作
基本数据类型数组的操作方法比较多,大致可以分为如下几类:
获得指定类型的数组:
j<Type>*Get<Type>ArrayElements(j<Type>Array array, jboolean *isCopy);
释放数组:
voidRelease<Type>ArrayElements(j<Type>Array array, j<Type>*elems, jint mode);
这类函数可以把Java基本类型的数组转换到C/C++中的数组。有两种处理方式,一是拷贝一份传回本地代码,另一种是把指向Java数组的指针直接传回到本地代码,处理完本地化的数组后,通过Realease<Type>ArrayElements来释放数组。处理方式由Get方法的第二个参数isCopied来决定(取值为JNI_TRUE或JNI_FLASE)。
其第三个参数mode可以取下面的值:
l 0:对Java的数组进行更新并释放C/C++的数组
l JNI_COMMIT:对Java的数组进行更新但是不释放C/C++的数组
l JNI_ABORT:对Java的数组不进行更新,释放C/C++的数组
例如:
package com.test.exam4_5
class ArrayTest {
static{
System.loadLibrary("native_array");
}
privateint [] arrays=new int[]{1,2,3,4,5};
publicnative void show();
publicstatic void main(String[] args) {
new ArrayTest ().show();
}
}
本地代码:
void Java_com_test_exam4_5_ArrayTest_show(JNIEnv * env, jobject obj)
{
jfieldID id_arrsys = env->GetFieldID(env->GetObjectClass(obj),"arrays", "[I");
jintArrayarr = (jintArray)(env->GetObjectField(obj, id_arrsys));
jint*int_arr = env->GetIntArrayElements(arr, NULL);
jsizelen = env->GetArrayLength(arr);
for(int i = 0; I < len; i++)
{
cout << int_arr[i]<< endl;
}
env->ReleaseIntArrayElements(arr, int_arr, JNI_ABORT);
}
Java代码与本地代码里在进行参数传递与返回值复制的时候,要注意数据类型的匹配。对于int, char等基本类型直接进行拷贝即可,对于Java中的对象类型,通过传递引用实现。JVM保证所有的Java对象正确的传递给了本地代码,并且维持这些引用,因此这些对象不会被Java的gc(垃圾收集器)回收。因此,本地代码必须有一种方式来通知JVM本地代码不再使用这些Java对象,让gc来回收这些对象。
JNI将传递给本地代码的对象分为两种:局部引用和全局引用。
l 局部引用:只在上层Java调用本地代码的函数内有效,当本地方法返回时,局部引用自动回收。
l 全局引用:只有显示通知VM时,全局引用才会被回收,否则一直有效,Java的gc不会释放该引用的对象。
JNI中对于局部引用和全局引用相关的函数如下:
创建指向某个对象的局部引用,创建失败返回NULL
jobject NewLocalRef(jobject ref);
删除局部引用
void DeleteLocalRef(jobject obj);
创建指向某个对象的全局引用,创建失败返回NULL
jobject NewGlobalRef(jobject lobj);
删除全局引用
void DeleteGlobalRef(jobject gref);
默认的话,传递给本地代码的引用是局部引用。所有的JNI函数的返回值都是局部引用。
jstring MyNewString(JNIEnv *env, jchar *chars,jint len)
{
staticjclassstringClass = NULL; //static 不能保存一个局部引用
jmethodIDcid;
jcharArrayelemArr;
jstringresult;
if(stringClass== NULL) {
stringClass =env->FindClass("java/lang/String"); // 局部引用
if(stringClass == NULL) {
return NULL; /* exception thrown */
}
}
/* 本地代码中创建的字符串为局部引用,当函数返回后字符串有可能被gc回收 */
cid =env->GetMethodID(stringClass,"<init>","([C)V");
result=env->NewStringUTF(stringClass, cid, “Hello World”);
returnresult;
}
虽然局部引用会在本地代码执行之后自动释放,但是有下列情况时,要手动释放:
l 本地代码访问一个很大的Java对象时,在使用完该对象后,本地代码要去执行比较复杂耗时的运算时,由于本地代码还没有返回,Java收集器无法释放该本地引用的对象,这时,应该手动释放掉该引用对象。
l 本地代码创建了大量局部引用,这可能会导致JNI局部引用表溢出,此时有必要及时地删除那些不再被使用的局部引用。比如:在本地代码里创建一个很大的对象数组。
jni.h头文件中定义了JNI本地方法与Java方法映射关系结构体JNINativeMethod。
l 创建的工具函数,它会被未知的代码调用,在工具函数里使用完的引用要及时释放。
l 不返回的本地函数。例如,一个可能进入无限事件分发的循环中的方法。此时在循环中释放局部引用,是至关重要的,这样才能不会无限期地累积,进而导致内存泄露。局部引用只在创建它们的线程里有效,本地代码不能将局部引用在多线程间传递。一个线程想要调用另一个线程创建的局部引用是不被允许的。将一个局部引用保存到全局变量中,然后在其它线程中使用它,这是一种错误的编程。
在一个本地方法被多次调用时,可以使用一个全局引用跨越它们。一个全局引用可以跨越多个线程,并且在被程序员手动释放之前,一直有效。和局部引用一样,全局引用保证了所引用的对象不会被垃圾回收。
JNI允许程序员通过局部引用来创建全局引用, 全局引用只能由NewGlobalRef函数创建。下面是一个使用全局引用例子:
jstringMyNewString(JNIEnv *env, jchar *chars,jint len)
{
staticjclassstringClass = NULL;
...省略部分代码
if(stringClass == NULL) {
jclasslocalRefCls =env->FindClass("java/lang/String");
if(localRefCls == NULL) {
return NULL;
}
/*创建全局引用并指向局部引用 */
stringClass = env->NewGlobalRef(localRefCls);
/*删除局部引用 */
env->DeleteLocalRef(localRefCls);
/*判断全局引用是否创建成功 */
if(stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}
}
在native代码不再需要访问一个全局引用的时候,应该调用DeleteGlobalRef来释放它。如果调用这个函数失败,Java VM将不会回收对应的对象。
本地代码在某次被调用时生成的对象,在其他函数调用时是不可见的。虽然可以设置全局变量但那不是好的解决方式,Android中通常是在Java域中定义一个int型的变量,在本地代码生成对象的地方,与这个Java域的变量关联,在别的使用到的地方,再从这个变量中取值。
以JNICameraContext为例来说明:
JNICameraContext是android_hardware_camera.cpp中定义的类型,并会在本地代码中生成对象并与Java中定义的android.hardware.Camera类的mNativeContext整形成员关联。
注:为了简化理解,代码已经做了简单修改
static voidandroid_hardware_Camera_native_setup(JNIEnv*env, jobject thiz,jobjectweak_this, jintcameraId)
{
// 创建JNICameraContext对象
JNICameraContext *context = new JNICameraContext(env, weak_this,clazz,camera);
…省略部分代码
// 查找到Camera类
jclassclazz =env->FindClass("android/hardware/Camera ");
// 保存Carmera类中mNativeContext成员ID
jfieldID field = env->GetFieldID(clazz,"mNativeContext","I");
// 保存context对象的地址到了Java中的mNativeContext属性里
env->SetIntField(thiz,fields, (int)context);
}
当要使用在本地代码中创建的JNICameraContext对象时,通过JNIEnv::GetIntField()获取Java对象的属性,并转化为JNICameraContext类型:
// 查找到Camera类
jclassclazz =env->FindClass("android/hardware/Camera ");
// 保存Carmera类中mNativeContext成员ID
jfieldID field = env->GetFieldID(clazz,"mNativeContext","I");
// 从Java环境中得到保存在mNativeContext中的对象引用地址
JNICameraContext*context=(JNICameraContext*)(env->GetIntField(thiz,field));
if(context!= NULL) {
//…省略部分代码
}
前面我们介绍了Java方法和本地方法互相调用过程中用到的JNI接口函数。在Java代码调用本地方法时,JVM又是如何能正确的绑定到本地方法呢?
当JVM在调用带有native关键字的方法时,JVM在Java运行时环境中查找“一张方法映射表”,根据这张表寻找对应的本地方法,如果本地代码中没有找到对应的函数,则会抛出java.lang.UnsatisfiedLinkError错误,所以,当我们在使用JNI编程时,必须保证本地方法出现在“方法映射表”中。
本地代码最终编译成动态库,在Java代码中通过System.loadLibrary方法来加载本地代码库,当本地代码动态库被JVM加载时,JVM会自动调用本地代码中的JNI_OnLoad函数。
JNI_OnLoad函数的定义如下:
jint JNI_OnLoad(JavaVM *vm, void *reserved);
参数说明:
vm:代表了JVM实例,其实是和JVM相关的一些操作函数指针,详情请查看4.2.1章节。
reserved:保留
一般来说JNI_OnLoad函数里主要做以下工作:
l 调用GetEnv函数,获得JNIEnv,即Java执行环境
l 通过RegisterNatives函数注册本地方法
l 返回JNI版本号
JNI从Java1.0到现在其版本也在发生变化 ,变化主要体现在JNIEnv中支持的函数个数,当调用GetEnv函数时可以指定获得某个版本的JNIEnv函数表。
jint GetEnv(void **penv, jint version);
参数说明:
penv: JNIEnv指针的地址,GetEnv成功调用后,它指向JNIEnv指针。
version:请求的JNI版本号,如:JNI_VERSION_1_4,表示请求JNI1.4版本的JNIEnv执行环境。
返回值:当请求的JNI版本号不支持时,返回负值,成功返回JNI_OK。
RegisterNatives是JNIEnv所提供的功能函数,用于注册本地方法和Java方法的映射关系到JVM中,保证Java代码调用本地代码时能正确调用到本地代码。
JVM要求JNI_OnLoad函数必须返回一个合法的JNI版本号,表示该库将被JVM加载,因此本地代码的JNI_OnLoad的实现一般如下:
/*
* Thisiscalled by the VM when the shared library is first loaded.
*/
jintJNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env= NULL;
jintresult= -1;
if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { // 调用GetEnv请求获得指定版本的JNIEnv
gotofail;
}
if(env!= NULL)
gotofail;
if(registerMethods(env) != 0) { //调用子函数注册本地方法
gotofail;
}
/*success-- return valid version number */
result =JNI_VERSION_1_4; //指定返回值为合法的JNI版本号
fail:
return result;
}
RegisterNatives通常在本地代码被加载时被调用,用来将JNI映射关系“告诉”Java执行环境。映射关系其实是在jni.h中定义一个结构体:
@jni.h
typedef struct {
char *name; // Java方法名
char *signature; //方法签名表示字符串
void *fnPtr; // Java方法对应的本地函数指针
} JNINativeMethod;
该结构体记录了Java运行环境中Java方法名name,Java方法签名signature以及其对应的本地函数名fnPtr。
由于Java代码中可能定义多个本地方法,所以JNINativeMethod结构通常放到一个数组中,通过RegisterNatives注册到JVM中:
@jni.h
jint RegisterNatives(jclass clazz, constJNINativeMethod *methods, jint nMethods);
jint UnregisterNatives(jclass clazz);
clazz:成员方法属于某个类,clazz指定注册的映射关系所在的类
methods:JNINativeMethod指针,它通常是一个映射关系数组
nMethods:映射关系数组元素个数,即:映射关系数量
当这些映射关系不再需要时,或需要更新映射关系时,则调用UnregisterNatives函数,删除这些映射关系。
我们现在完整的来看下之前例子:
@ NativeTest.java
package com.test.exam2;
class NativeTest{
static{
System.loadLibrary(“native_call”);
}
privatenative static void callNativePrint(String str);
privatenative static intcallNativeAdd(int n1, int n2);
publicstatic void main(String arg[]){
callNativePrint(“HelloWorld”);
System.out.println(“n1+ n2 = ” + callNativeAdd(10, 20));
}
}
@com_test_exam2_NativeTest.cpp:
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <jni.h>
#include <jni_md.h>
void native_print(JNIEnv * env, jclassthiz, jobject obj)
{
printf(“Stringfrom java:%s\n”, env->GetStringUTFChars((jstring)obj, JNI_FALSE));
}
jint native_add(JNIEnv * env, jclassthiz, jintn1, jint n2)
{
returnn1 + n2;
}
/*
* 定义映射关系结构体数组
*/
static const JNINativeMethod gMethods[] = {
{"callNativePrint", "(Ljava/lang/String;)V",(void*)native_print},
{"callNativeAdd", "(II)I",(void*)native_add},
};
/*
* 将映射关系结构体数组注册到JVM中
*/
static int registerMethods(JNIEnv*env) {
static constchar* const className= " com/test/exam2/NativeTest";
jclass clazz;
/* look upthe class */
clazz =env->FindClass(className);
if (clazz ==NULL) {
return-1;
}
/* registerall the methods */
if(env->RegisterNatives(clazz,gMethods,
sizeof(gMethods) /sizeof(gMethods[0])) != JNI_OK)
{
return -1;
}
/* fill outthe rest of the IDcache */
return 0;
}
/*
* Thisiscalled by the VM when the shared library is first loaded.
*/
jintJNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env= NULL;
jintresult= -1;
if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { // 调用GetEnv请求获得指定版本的JNIEnv
gotofail;
}
if(env!= NULL)
gotofail;
if(registerMethods(env) != 0) { //调用子函数注册本地方法
gotofail;
}
/*success-- return valid version number */
result =JNI_VERSION_1_4; //指定返回值为合法的JNI版本号
fail:
return result;
}
【实验内容】
在Linux操作系统中硬件通常有:open,read,write,close等相关操作接口,每个设备硬件还有一些自己的属性,我们用Java编写一个Screen“屏幕”设备类,设备的初始化,设备打开,关闭,读/写都交给本地代码去实现。当写屏幕设备时,将写入内容存放在本地代码缓冲区中,当读屏幕设备时,则将数据经过简单处理读取出来。例如:向Screen中写入a-z的小写字母,读出来变成A-Z的大写。
在Ubuntu系统中编写Java程序和本地C++代码,编写Makefile文件用来编译Java代码和本地代码库,最终正常运行Java与C++本地代码。
【实验目的】
通过实验,学员掌握在Linux系统中编写基于JNI的java程序和与Java对应的C++本地代码,熟悉Linux中编译动态库的过程和Makefile的编写,最终掌握JNI编程相关知识。
【实验平台】
安装有JDK的Ubuntu操作系统(可以在Windows系统中虚拟Ubuntu系统)。
【实验步骤】
1. 设计Screen类,并实现其代码
@Screen.java
package com.test.practice4_8;
class Screen{
// Load libscreen.so lib
static{
System.loadLibrary("screen");
}
private String mDevName;
private int mDevNo;
private boolean isInit = false;
private int mWidth;
private int mHeight;
publicScreen(){
mDevName = null;
mDevNo = 0;
}
// check is device inital
publicboolean isInit(){
return isInit;
}
// get the screen width
publicint getWidth(){
return mWidth;
}
// get the screen height
publicint getHeight(){
return mHeight;
}
// print screen informations
publicvoid printInfo(){
System.out.println("Screen Name: " + mDevName);
System.out.println("Device No: " + mDevNo);
System.out.println("Screen width: " + mWidth);
System.out.println("Screen height:" + mHeight);
}
// define all native methods
publicnative boolean open();
publicnative int read(byte[] data, int len);
publicnative int write(byte[] data, int len);
publicnative void close();
}
2. 设计并实现Screen测试类
package com.test.practice4_8;
class ScreenTest{
publicstatic void main(String arg[]){
Screen dev = new Screen(); //创建Screen对象
if(!dev.open()){ //打开Screen设备
System.out.println("Screen open error");
return;
}
dev.printInfo(); //打印设备信息
byte[] data = new byte[26]; //定义要写入的数据,不能用双字节的char类型
for(int i = 0; i < 26; i++){
data[i] = (byte)(97 + i);
}
System.out.println("Write a-z to Screen device:");
dev.write(data, data.length); // 写入设备中
byte[] buf = new byte[64];
int size = dev.read(buf, buf.length); //从设备中读取出来
if(size < 0){
System.out.println("read data from screen device error");
return ;
}
System.out.println("Read data from Screen device:");
for(int i = 0; i < 26; i++){
System.out.print((char)buf[i] + ","); // 打印出读取出的数据
}
System.out.println();
dev.close(); //关闭设备
}
}
3. 设计并实现本地代码
@com_test_practice4_8_ScreenTest.cpp
#include <unistd.h>
#include <stdlib.h>
#include <malloc.h>
#include <jni.h>
#include <jni_md.h>
// 定义一个全局结构体,用来保存所有的Screen类的相关ID信息
struct screen{
jclassclazz;
jfieldID id_dev_name;
jfieldID id_dev_no;
jfieldID id_is_init;
jfieldIDid_width;
jfieldID id_height;
} *gfieldID;
// 定义读写数据的缓冲区
char _data[64];
//初始化Screen类的相关ID信息,起到缓存的作用
static int native_id_init(JNIEnv *env){
gfieldID = (struct screen*)malloc(sizeof(struct screen));
if(gfieldID == NULL)
return -1;
gfieldID->clazz =env->FindClass("com/test/practice4_8/Screen");
gfieldID->id_dev_name = env->GetFieldID(gfieldID->clazz,"mDevName", "Ljava/lang/String;");
gfieldID->id_dev_no = env->GetFieldID(gfieldID->clazz,"mDevNo", "I");
gfieldID->id_is_init= env->GetFieldID(gfieldID->clazz, "isInit", "Z");
gfieldID->id_width = env->GetFieldID(gfieldID->clazz,"mWidth", "I");
gfieldID->id_height = env->GetFieldID(gfieldID->clazz,"mHeight", "I");
return 0;
}
// Java代码中open方法的本地实现
static jboolean native_open(JNIEnv * env,jobject thiz) {
//init the jfieldID in Java env
if(native_id_init(env) != 0){
return JNI_FALSE;
}
// 创建设备名字符串
jstring dev_nm = env->NewStringUTF("Farsight HD LCDScreen");
if(dev_nm == NULL)
return JNI_FALSE;
// 写回Screen对象的mDevName属性里
env->SetObjectField(thiz, gfieldID->id_dev_name, dev_nm);
// 设备设备号
env->SetIntField(thiz, gfieldID->id_dev_no, 0x1234);
// 设置初始化标识
env->SetBooleanField(thiz, gfieldID->id_is_init, JNI_TRUE);
// 设置Screen宽度
env->SetIntField(thiz, gfieldID->id_width, 1023);
// 设置Screen高度
env->SetIntField(thiz, gfieldID->id_height, 768);
returnJNI_TRUE;
}
// Screen类 read方法的本地实现
static jint native_read(JNIEnv * env, jobjectthiz, jbyteArray arr, jint len)
{
if(len<= 0){
return len;
}
// 获得Java层定义的byte数组
jbyte*byte_arr = env->GetByteArrayElements(arr, NULL);
int i= 0;
for(;i < len; i++){
byte_arr[i]= _data[i] - 32; // 将处理过的数据写回Java byte数组里
}
env->ReleaseByteArrayElements(arr, byte_arr, 0); // update array data andrelease array
returni;
}
// Screen类write方法的本地实现
static jint native_write(JNIEnv * env, jobjectthiz, jbyteArray arr, jint len)
{
if(len> sizeof(_data) && len <= 0){
return -1;
}
// 获得Java层定义的byte数组
jbyte*byte_arr = env->GetByteArrayElements(arr, NULL);
int i= 0;
for(;i < len; i++){
_data[i] = byte_arr[i]; //将Java byte数组保存在本地缓存区中
printf("%c,", _data[i]);
}
printf("\n");
env->ReleaseByteArrayElements(arr, byte_arr, JNI_ABORT); // do not update array data release array
returni;
}
// Screen类close方法的本地实现
static void native_close(JNIEnv * env, jobjectthiz)
{
// 修改isInit的值为false
env->SetBooleanField(thiz, gfieldID->id_is_init, JNI_FALSE);
free(gfieldID); //释放空间
gfieldID = NULL;
}
/*
* 定义映射关系结构体数组
*/
static const JNINativeMethod gMethods[] = {
{"open", "()Z", (void*)native_open},
{"read", "([BI)I", (void*)native_read},
{"write", "([BI)I", (void*)native_write},
{"close", "()V", (void*)native_close},
};
/*
* 将映射关系结构体数组注册到JVM中
*/
static int registerMethods(JNIEnv* env) {
staticconst char* const className = "com/test/practice4_8/Screen";
jclassclazz;
/*look up the class */
clazz= env->FindClass(className);
if(clazz == NULL) {
printf("FindClass error\n");
return-1;
}
/*registerall the methods */
if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods) /sizeof(gMethods[0])) != JNI_OK)
{
return -1;
}
/*fill outthe rest of the ID cache */
return0;
}
/*
* This iscalled by the VM when the sharedlibrary is first loaded.
*/
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env= NULL;
jintresult= -1;
if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { // 调用GetEnv请求获得指定版本的JNIEnv
printf("GetEnv error\n");
goto fail;
}
if(env== NULL)
goto fail;
if(registerMethods(env) != 0) { // 调用子函数注册本地方法
printf("registerMethods error\n");
goto fail;
}
/*success-- return valid version number */
result= JNI_VERSION_1_4; // 指定返回值为合法的JNI版本号
fail:
returnresult;
}
我们在本地代码中声明了一个全局结构体指针gfieldID,该结构体里面存放的是Screen类成员ID,因为这些ID要在后面的方法中频繁的使用,如果不缓存起来,意味着每次使用都要Findclass,GetFieldID,这对性能有很大影响。
4. 为了方便编译,编写Makefile
libscreen.so:com_test_practice4_8_ScreenTest.cpp ScreenTest.class
g++-I/home/linux/jdk1.5.0_21/include/ -I/home/linux/jdk1.5.0_21/include/linux/$< -fPIC -shared -o $@
ScreenTest.class: ScreenTest.java
javac-d ./ $<
clean:
$(RM)ScreenTest.class libscreen.so
由于本地代码要编译成so动态库,所以g++的参数要指定-fPIC –shared等选项,另外,在编译本地代码时要用到jni.h和jni_md.h头文件,所以还要加上-I选项,用来指定这两个头文件的位置,它们在我们安装的JDK的目录下。
细心的同学可能已经注意到,本地C++文件名为Java的包名+类名.cpp,包名不是以“.”作为间隔符,而是以目录间隔符“/”分隔,这也是因为Java中的包名本身就是使用目录名以区分命名空间。这样做还有另外一个好处,即:我们看到本地代码文件时基本上就可以通过文件找到其对应的Java代码,反之亦然。
5. 执行make命令,并且运行查看实验结果
$ make
$ java -Djava.library.path='.'com/test/practice4_8/ScreenTest
Java命令的“-Djava.library.path”选项表示指定在运行Java代码时,加载本地库时的寻找路径。为了避免每次都输入上述运行命令,我们可以写到一个脚本中
@run.sh
#!/bin/bash
java -Djava.library.path='.'com/test/practice4_8/ScreenTest
运行结果如下:
linux@ubuntu:~/jni/practice$ ./run.sh
Screen Name: Farsight HD LCD Screen
Device No: 4660
Screen width: 1023
Screen height:768
Write a-z to Screen device:
a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,
Read data from Screen device:
A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,
6. 常见问题
l Exception in thread"main" java.lang.NoClassDefFoundError: xxx
一般是由于FindClass方法查找不到Java类造成的,检查FindClass的参数是否正确。
l Exception in thread"main" java.lang.NoSuchMethodError: xxx
Java与本地方法的链接映射时出现错误,先确认下Java中有没有对应xxx方法声明,如果有,确认RegisterNatives注册映射关系的签名是否匹配。
l Exception in thread"main" java.lang.NoSuchFieldError:xxx
这表示在本地代码中访问xxx属性时,在java代码中没有该属性,先确认该属性是否定义,如果有定义,看下属性是静态属性还是非静态属性,如果是静态属性,本地方法只能通过Get/SetStatic<Type>Field来访问,如果是非静态属性,本地方法只能通过Get/Set<Type>Field来访问。
l Exception in thread"main" java.lang.UnsatisfiedLinkError: no xxx in java.library.path
这表示本地代码库找不到,确认java在执行时,“-Djava.library.path”参数是否正确。