首页 > 代码库 > 深入浅出 - Android系统移植与平台开发(十二)- Android JNI机制
深入浅出 - Android系统移植与平台开发(十二)- Android JNI机制
第五章、JNI机制
4.1 JNI概述
由前面基础知识可知,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代码实现一次。直接复用该本地代码,即提高了编程效率,又确保了程序的安全性和健壮性。
4.2 JNI原理
在计算机中,每种编程语言都有一个运行环境(Runtime),运行环境用来解释运行语言中的语句。不同的编程语言的运行环境就好比方西游记中的“阴阳两界”一样,一般人不能同一时候生存在阴阳两界中。仅仅有“黑白无常”能自由穿梭在阴阳两界之间。“黑白无常”往返于阴阳两界时手持生死簿。“黑白无常”按生死簿上记录着的人名“索魂”。
4.2.1 JavaVM与JNIEnv
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中的方法调用转换成对本地语言的函数调用。
4.3 JNI中的数据传递
4.3.1 JNI基本类型
当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接口调用时,要在内部都要转换。我们在使用的时候也须要小心。
4.3.2 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.4 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(); // 由于该方法的实如今本地代码中,所以加上nativekeyword进行声明
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类型的吗?
Nativekeyword本身和abstractkeyword冲突。他们都是方法的声明,仅仅是一个是把方法实现移交给子类,还有一个是移交给本地代码库。假设同一时候出现。就相当于即把实现移交给子类,又把实现移交给本地操作系统,那究竟谁来实现详细方法呢?
4.5 JNI訪问Java成员
在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方法。
4.5.1取得Java属性ID和方法ID
为了在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()方法。
4.5.2 JNI类型签名
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 |
4.5.3JNI操作Java属性和方法
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中构造方法是一种特殊的方法,主要用于对象创建时被回调。我们将在下一节分析。
4.5.4 在本地代码中创建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变量。即要释放的本地字符串的资源
第二个參数就是要释放的本地字符串
4.5.5 Java数组在本地代码中的处理
我们能够使用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);
}
4.6 局部引用与全局引用
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);
4.6.1局部引用
默认的话,传递给本地代码的引用是局部引用。
全部的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 不返回的本地函数。
比如,一个可能进入无限事件分发的循环中的方法。
此时在循环中释放局部引用,是至关重要的,这样才干不会无限期地累积。进而导致内存泄露。
局部引用仅仅在创建它们的线程里有效。本地代码不能将局部引用在多线程间传递。一个线程想要调用还有一个线程创建的局部引用是不被同意的。
将一个局部引用保存到全局变量中,然后在其他线程中使用它。这是一种错误的编程。
4.6.2 全局引用
在一个本地方法被多次调用时,能够使用一个全局引用跨越它们。
一个全局引用能够跨越多个线程,而且在被程序猿手动释放之前,一直有效。和局部引用一样。全局引用保证了所引用的对象不会被垃圾回收。
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将不会回收相应的对象。
4.6.3 在Java环境中保存JNI对象
本地代码在某次被调用时生成的对象,在其他函数调用时是不可见的。
尽管能够设置全局变量但那不是好的解决方案,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) {
//…省略部分代码
}
4.7本地方法的注冊
前面我们介绍了Java方法和本地方法互相调用过程中用到的JNI接口函数。在Java代码调用本地方法时,JVM又是怎样能正确的绑定到本地方法呢?
当JVM在调用带有nativekeyword的方法时,JVM在Java运行时环境中查找“一张方法映射表”。依据这张表寻找相应的本地方法。假设本地代码中没有找到相应的函数。则会抛出java.lang.UnsatisfiedLinkError错误,所以,当我们在使用JNI编程时。必须保证本地方法出如今“方法映射表”中。
4.7.1 JNI_OnLoad方法
本地代码终于编译成动态库,在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;
}
4.7.2 RegisterNatives方法
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;
}
4.8 JNI调用实验
【实验内容】
在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 = http://www.mamicode.com/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”參数是否正确。
深入浅出 - Android系统移植与平台开发(十二)- Android JNI机制