JNI是Java Native Interface的缩写,从Java1.1开始就成为了Java标准的一部分。JNI的定义了一种管理方式使Java能够与C/C++互相调用;它支持从动态链接库中loading code,且工作效率尚可。使用JNI调用现有的本地库,极大灵活了Java的开发(Java本身编写底层应用实现比较困难)。
JNI中的一些概念
-
native : Java语言中修饰本地方法的修饰符, 被该修饰符修饰的方法没有方法体;
-
Native方法 : 在Java语言中被native关键字修饰的方法是Native方法;
-
JNI层 : Java声明Native方法的部分;
-
JNI函数 : JNIEnv提供的函数, 这些函数在jni.h中进行定义;
-
JNI方法 : Native方法对应的JNI层实现的 C/C++方法, 即在jni目录中实现的那些C语言代码;
C与Java如何交流
-
JNI规范 : C语言与Java语言交流需要一个适配器, 中间件, 即 JNI, JNI提供了一种规范;
-
C语言中调用Java方法 : 可以让我们在C代码中找到Java代码class中的方法, 并且调用该方法;
-
Java语言中调用C语言方法 : 同时也可以在Java代码中, 将一个C语言的方法映射到Java的某个方法上;
-
JNI桥梁作用 : JNI提供了一个桥梁, 打通了C语言和Java语言之间的障碍;
JNI作用 :
-
扩展: JNI扩展了JVM能力, 驱动开发, 例如开发一个wifi驱动, 可以将手机设置为无限路由;
-
高效 : 本地代码效率高, 游戏渲染, 音频视频处理等方面使用JNI调用本地代码, C语言可以灵活操作内存;
-
复用 : 在文件压缩算法 7zip开源代码库, 机器视觉 openCV开放算法库 等方面可以复用C平台上的代码, 不必在开发一套完整的Java体系, 避免重复发明轮子;
-
特殊 : 产品的核心技术一般也采用JNI开发, 不易破解;
Java语言执行流程 :
-
编译字节码 : Java编译器编译 .java源文件, 获得.class 字节码文件;
-
装载类库 : 使用类装载器装载平台上的Java类库, 并进行字节码验证;
-
Java虚拟机 : 将字节码加入到JVM中, Java解释器 和 即时编译器 同时处理字节码文件, 将处理后的结果放入运行时系统;
-
调用JVM所在平台类库 : JVM处理字节码后, 转换成相应平台的操作, 调用本平台底层类库进行相关处理;
网上JNI的文章不多,我将自己的一些总结写在这里。如有错误,欢迎指出。
Java调用C/C++总结起来有如下三个步骤:
-
编写带有native方法的Java类,使用javac工具编译Java类
-
使用javah工具来生成与native方法对应的头文件(一般为XX.h文件)
-
实现这个头文件,并将其编译为动态链接库(.dll或.so)
下面是一个简单完整的Java调用C/C++的例子,改自IBM的一篇介绍JNI的文章和icejoywoo的博客。
编写Java类
我们来编写一个JNITest的Java类。
public class JNITest {
public native int intFunc(int n); //参数类型为int
public native boolean booleanFunc(boolean bool); //参数类型为boolean
public native String stringFunc(String text); //参数类型为String
public native int intArrayFunc(int[] intArray); //参数类型为int
public static void main (String[] args) {
//加载了动态类库,在windows下加载.dll,在Linux下加载.so,决不可加上后缀,要保证JNITest在path路径中
System.loadLibrary("JNITest");
JNITest jnitest = new JNITest();
int square = jnitest.intFunc(5);
boolean boolv = jnitest.booleanFunc(true);
String text = jnitest.stringFunc("Hello JNI");
int sum = jnitest.intArrayFunc(new int[]{1,2,3,4,5,6,7,8,9,0});
System.out.println("intFunc: " + square);
System.out.println("booleanFunc: " + boolv);
System.out.println("stringFunc: " + text);
System.out.println("intArrayFunc: " + sum);
}
}
上面代码中的4个native方法就是我们需要用C来实现的方法。
编译JNITest文件,可以看到JNITest.class文件。
javah工具生成头文件
命令行运行
javah JNITest
可以看到目录下自动生成了一个头文件JNITest.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNITest */
#ifndef _Included_JNITest
#define _Included_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JNITest
* Method: intFunc
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_JNITest_intFunc
(JNIEnv *, jobject, jint);
/*
* Class: JNITest
* Method: booleanFunc
* Signature: (Z)Z
*/
JNIEXPORT jboolean JNICALL Java_JNITest_booleanFunc
(JNIEnv *, jobject, jboolean);
/*
* Class: JNITest
* Method: stringFunc
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JNITest_stringFunc
(JNIEnv *, jobject, jstring);
/*
* Class: JNITest
* Method: intArrayFunc
* Signature: ([I)I
*/
JNIEXPORT jint JNICALL Java_JNITest_intArrayFunc
(JNIEnv *, jobject, jintArray);
#ifdef __cplusplus
}
#endif
#endif
可以看到有4个函数声明,命名方式为 Java_完整类名_方法名,完整类名包括包名,假设我的demo.JNITest是完整类名,对应的就是demo_JNITest(本文不是)。 注释中有个方法签名(Signature),通过一个表格来介绍Signature。
Java类型 | Signature | 备注 |
---|---|---|
boolean | Z | |
byte | B | |
char | C | |
short | S | |
int | I | |
long | L | |
float | F | |
double | D | |
void | V | |
object | L用/分割的完整类名 | 例如:Ljava/lang/String表示String类型 |
Array | [签名 | 例如:[I 表示int数组,[Ljava/lang/String表示String数组 |
Method | (参数签名) 返回类型签名 | 例如:([I)I 表示参数类型为int数组,返回int类型的方法 |
上面头文件的第一个函数声明
JNIEXPORT jint JNICALL Java_Sample1_intMethod (JNIEnv *, jobject, jint);
注释中的签名是 Signature: (I)I
实现头文件中的函数
可以使用C/C++语言实现,下面是使用C语言实现的版本(其实C/C++)实现起来差不多。
#include "JNITest.h"
#include <string.h>
/* JNIEnv*:是每个函数都有的参数,代表的是Java环境, 通过这个环境可以调用Java里面的方法;
*jobject参数: 调用C语言方法的对象, this对象表示当前的对象, jobject 指向在此 Java 代码中实例化的 Java 对象 LocalFunction 的一个句柄,相当于 this 指针即调用JNI方法所在的类;
*
*/
JNIEXPORT jint JNICALL Java_JNITEST_intFunc(JNIEnv *env, jobject obj, jint num) {
return num * num;
}
JNIEXPORT jboolean JNICALL Java_JNITEST_booleanFunc(JNIEnv *env, jobject obj, jboolean boolean) {
return !boolean;
}
JNIEXPORT jstring JNICALL Java_JNITEST_stringFunc(JNIEnv *env, jobject obj, jstring string) {
//GetStringUTFChars()这个方法,是Java和C/C++之间转换字符串,Java本身应的是双字节字符,而C使用的是单字节字符,故需要转换
const char* str = (*env)->GetStringUTFChars(env, string, 0);
char cap[128];
strcpy(cap, str);
//ReleaseStringUTFChars()是用来释放对象的, 在Java中有虚拟机进行垃圾回收, 但是在C语言中, 这些对象必须手动回收. 否则可能造成内存泄漏.
(*env)->ReleaseStringUTFChars(env, string, 0);
return (*env)->NewStringUTF(env, strupr(cap));
}
JNIEXPORT jint JNICALL Java_JNITEST_intArrayFunc(JNIEnv *env, jobject obj, jintArray array) {
int i, sum = 0;
jsize len = (*env)->GetArrayLength(env, array);
jint *body = (*env)->GetIntArrayElements(env, array, 0);
for (i = 0; i < len; i++) {
sum += body[i];
}
return sum;
}
C++的代码和C的很像,只有一个不同点
C代码: (*env)->GetStringUTFChars(env, string, 0);
C++代码: env->GetStringUTFChars(string, 0);
C语言中使用的是结构体的函数指针, 而在C++中使用的还是struct, 我们知道struct在C++中和class的功能是几乎一样的, struct也可以用来定义类, 所以env在C++中是个类对象的指针.
编译和运行
MS的编译器,编译C语言版本的.dll
用VS新建dll工程,编译即可。
Linux的gcc编译器,编译C语言版本的.so
gcc命令编译
gcc -I /usr/lib/jvm/java-6-sun/include/linux/ -I /usr/lib/jvm/java-6-sun/include/ -I /home/mike_ming/workspace/JNI/ -fPIC -shared -o libJNITest.so JNIC.c
注意:
- -I /home/mike_ming/workspace/JNI/ 这个是指当前有JNITest.h文件的目录
- Linux规定库文件(.so)必须以lib开头,但是我们使用loadlibrary方法加载的是JNITest
运行
java -Djava.library.path=’.’ JNITest
-Djava.library.path 代表的就是当前的.so路径了
** 引用 **