admin 管理员组

文章数量: 887016

深入浅出

第五章、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();    // 由于该方法的实现在本地代码中,所以加上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关键字冲突,他们都是方法的声明,只是一个是把方法实现移交给子类,另一个是移交给本地代码库。如果同时出现,就相当于即把实现移交给子类,又把实现移交给本地操作系统,那到底谁来实现具体方法呢?

 

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在调用带有native关键字的方法时,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 = 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”参数是否正确。



本文标签: 深入浅出