首页 > 代码库 > 深入浅出 - 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机制