在Android Framework中,需要提供一种媒介或桥梁,将Java层(上层)与C/C++(底层)有机地联系起来,使得它们相互协调,共同完成某些任务。在这两层之间充当连接桥梁这一角色的就是Java本地接口(JNI,Java Native Interface),它允许Java代码与基于C/C++编写的应用和库进行交互操作。
JNI提供了一系列接口,允许Java类与使用C/C++等其它编程语言(在JNI中,这些语言被称为本地语言)编写的应用程序、模块、库进行交互操作。比如,在Java类中使用C语言库中中的特定函数,或在C语言里面使用Java类库,都需要借助JNI来完成。
通常会在下列几种情况下使用JNI
- 注重处理速度:如果对某段程序的执行速度有较高的要求,建议使用C/C++编写代码,而后在Java层通过JNI调用基于C/C++编写的部分代码。
- 硬件控制:为了更好地控制硬件,硬件控制代码通常使用C语言编写,借助JNI将其与Java层连接起来,从而实现对硬件的控制。
- 已有C/C++代码的复用:在编写程序的过程中,常常会使用已经编写好的C/C++代码,既提高了编程效率,又确保了程序的安全性和健壮性。在复用这些C/C++代码时,就要通过JNI来实现。
在Java代码中通过JNI调用C函数的步骤如下:
- 第一步:编写Java代码
- 第二步:编译Java代码(javac Java文件)
- 第三步:生成C代码头文件(javah java类名,自动生成)
- 第四步:编写C代码(实现C代码头文件里面的函数)
- 第五步:生成C共享库(使用工具编译生成C共享库,win下面为dll文件,Linux下面为so文件)
- 第六步:运行Java程序(java 类名)
第一步:编写Java代码
首先编写调用C语言的Java源代码HelloJNI.java
public class HelloJNI { native void printHello(); 1 native void printString(String str); static{ System.loadLibrary("hellojni"); 2 } public static void main(String[] args) { // TODO Auto-generated method stub HelloJNI myJNI = new HelloJNI(); myJNI.printHello(); myJNI.printString("Hello world form printString function!"); } }
说明:
1在Java类中,使用”native”关键字,声明本地方法,该方法与用C/C++编写的JNI本地函数相对应。”native”关键字告知Java编译器,在Java代码中带有该关键字的方法只是声明,具体由C/C++等其它语言编写实现。
2在Java类中声明了本地方法之后,接下来,调用System.loadLibrary()方法,加载具体实现本地方法的C运行库(在Java中加载本地运行库通常使用静态块(static block))。System.loadLibrary()方法加载由字符串参数指定的本地库,在不同操作系统平台下,加载的C运行库不同。在Window下面,调用System.loadLibrary(“hellojni”),则hellojni.dll会被加载;在Linux下面,则会加载libhellojni.so文件。
第二步:编译Java代码
使用如下命令编译java源代码:
javac HelloJNI.java
编译好HelloJNI.java后,生成HelloJNI.class文件。如果此时直接运行java程序,就是抛出异常。
由于尚未创建加载到Java代码中的hellojni.dll库文件,无法找到Java虚拟机要加载的C运行库。
接下来,创建hellojni.dll库文件。
第三步:生成C代码头文件
若想创建本地方法的映射C函数,必须先生成函数原型,函数原型存在于C/C++头文件中。Java提供了javah工具,位于JAVA JDK的安装目录的bin目录下面,用来生成包含函数原型的C/C++头文件,使用方法如下:
javah <包含以native关键字声明方法的Java类名称>
运行javah命令,会在当前目录下生成与Java类名(即javah命令的参数)相同名称的C语言头文件。在生成的C头文件中,定义了与Java本地方法相链接的C函数原型。
以下为生成的HelloJNI.h文件内容:
/* DO NOT EDIT THIS FILE - it is machine generated */ 1 #include <jni.h> /* Header for class HelloJNI */ #ifndef _Included_HelloJNI #define _Included_HelloJNI #ifdef __cplusplus extern "C" { #endif /* * Class: HelloJNI * Method: printHello * Signature: ()V */ JNIEXPORT void JNICALL Java_HelloJNI_printHello (JNIEnv *, jobject); /* * Class: HelloJNI 2 * Method: printString * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_HelloJNI_printString (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
说明:
1该文件由javah命令生成,为了保证JNI正常运行,请不要直接修改本文件的内容,JNI开发者只要使用C/C++语言实现定义的函数即可。
2这是javah命令生成的两个C函数原型,函数原型在Java类中声明的本地方法的基础上生成。查看各函数原型注释,可以看到与各函数原型对应的Java代码中的本地方法,注释中标明了三个元素:类名、本地方法、本地方法签名。
接下来分析一下函数原型:JNIEXPORT、JNICALL都是JNI关键字,表示此函数要被JNI调用,函数原型中必须有这两个关键字,JNI才能正常调用函数。其实,JNIEXPORT、JNICALL两个关键字都是宏定义,在JDK_HOME/include/win32/jni_md.h文件(在Window平台下)中。
观察函数原型名称,可以发现函数名称遵循一定的命名规则:JNI支持的函数命名形式为”Java_类名_本地方法名”。通过函数命名即可推断出JNI本地函数与哪个Java类的哪个本地方法相对应。
在生成的函数原型中,带有两个默认参数,分别为JNIEnv * 与jobject,支持JNI的函数必须包含这两个共同参数。第一个参数JNIEnv *为JNI接口指针,用来调用JNI表中的各种JNI函数(这里的JNI函数是指JNI中提供的基本函数集);第二个参数jobject也是JNI提供的Java本地方法,用来在C代码中访问Java对象,此参数中保存着调用本地方法的对象的一个引用。
在JNI编程中,Java程序与C/C++函数间经常进行数据交换,如果不提供一种方法消除两种语言的数据类型的差异,那么程序就无法正常运行,运行的可靠性也无法保障。JNI提供了一套与Java数据类型相对应的Java本地类型,使得本地语言可以使用Java数据类型,如下表所示:
Java类型 |
本地类型 |
字节(bit) |
boolean |
jboolean |
8, |
byte |
jbyte |
8 |
char |
jchar |
16, |
short |
jshort |
16 |
int |
jint |
32 |
long |
jlong |
64 |
float |
jfloat |
32 |
double |
jdouble |
64 |
void |
void |
n/a |
以上Java本地类型定义在JDK_HOME/include/jni.h文件中。此外,Java本地类型也提供了另外三种类型,分别对应于Java类、对象与字符串三种引用类型数据(除此之外还有几种引用类型,有兴趣可以可以查阅相关资料)。
Java引用类型 |
Java本地类型 |
类 |
Jclass |
对象 |
Jobject |
String |
Jstring |
第四步:编写C代码
在C函数原型生成后,开始编写hellojni.c文件,具体实现JNI本地函数。首先,把定义在HelloJNI.h头文件中的函数原型复制到hellojni.c,注意在使用javah命名生成的头文件中,函数的参数仅指定了参数的类型,并未给出参数的名称,因此复制完函数原型,开始实现C函数时,必须先在参数类型后指定具体的参数名称。
以下为编写好的hellojni.c代码:
#include "HelloJNI.h" #include <stdio.h> //添加名称为env与obj的两个参数 JNIEXPORT void JNICALL Java_HelloJNI_printHello(JNIEnv *env, jobject obj) { printf("Hello world!\n"); return ; } JNIEXPORT void JNICALL Java_HelloJNI_printString(JNIEnv *env, jobject obj, jstring string) { //将Java String转换以C字符串 const char * str = (*env)->GetStringUTFChars(env,string,0); printf("%s!\n",str); return ; }
GetStringUTFChars ()是JNI函数,用来将Java字符串转换成C语言字符串。JNI提供了多种JNI函数,用来处理C字符串与Java字符串的转换,具体可以参考:
第五步:生成C共享库
在编写好了hellojni.c之后,使用编译器将其编译成hellojni.dll文件。这里使用的是Visual C++ 2008 Express Editions。安装好之后,使用Visual Studio 2008 命令提示,输入编译指令:
cl -I"F:\Java\jdk1.7.0\include" -I"F:\Java\jdk1.7.0\include\win32" -LD hellojni.c -Fehellojni.dll
执行结果为:
指令说明:
cl:visual c++编译命令
-I<dir>:添加要检索头文件的目录路径<dir>
为了检索头文件,添加如下目录
jni.h(<JDK_HONE>\include)
jni_md.h(JDK_HONE>\include\win32)
-LD:创建DLL
-Fe<文件名>:指定编译结果文件名称
第六步:运行Java程序
执行java指令,运行HelloJNI类。
附录:
生成JNI的DLL时提示找不到jni.h的解决办法Cannot open include file: 'jni.h': No such file or directory