JNI
简介
JNI(Java Native Interface),译作Java本地接口,是Java与Native原生层语言互通的桥梁。Native语言一般指C和Cpp。通过JNI特性,Java编写的函数可以调用Native(C、Cpp)语言编写的函数,同样,Native语言编写的函数也可以调用Java编写的函数。JNI是Java语言的特性,有非常广泛的应用场景,而非Android独有。Android NDK(Native Development Kit)是Android为了方便Android程序员利用JNI特性使Java语言和Native语言互通而发明的开发工具集。
何时使用JNI?
- 应用需要直接操作硬件,访问系统的特性和设备。JVM本身不跨平台,为了使JAVA实现跨平台操作,JVM将硬件操作全部封装起来。所以Java并不能进行驱动开发等操作,但Java可以通过JNI调用C语言进行驱动开发、操作硬件。使用JNI开发的应用不是平台无关的,除非将本地代码在不同的操作平台下编译出相应的动态库。
- 应用对性能(运行效率、安全性)要求比较高。
- 复用C、Cpp沉淀的优秀代码(如文件压缩、人脸识别)。
如何使用JNI?
- 在Java类中声明一个本地方法。
- 运行javah获得包含该方法的C声明的头文件(也可以不生成头文件,直接编写本地代码)
- 用C实现该本地方法
- 将代码置于共享类库中
- 在Java程序中加载该类库。
/*第1步*/
class HelloNative
{
public static native void greeting();
}
本地方法既可以是静态也可以是非静态的,此处本地方法被声明为静态方法。
/*第2-3步*/
/*不生成头文件的Native代码编写*/
#include"jni.h"
#include<stdio.h>s
Jstring Java_HelloNative_greeting(JNIEnv* enc,jclass cl)
{
printf("Hello Native World\n");
}
- Native中方法的命名方式:
- 使用完全限定方法名,若类属于某个包,则要包含包名。如HelloNative.greeting和com.exam.HelloNative.greeting。
- 将点号替换为下划线,并加上“Java_”前缀
- 如果类名中含有非ASCII字母或数字,用_0xxxx替换,xxxx是该字符的4个十六进制数字
可以使用javah工具自动生成函数名:
/*第2步*/
javac HelloNative.class
javah HelloNative
/*HelloNative.h*/
#include<jni.h>
……省略……
JNIEXPORT void JNICALL Java_helloNative_greeting(JNIEnv*,jclass);
……省略……
生成的HelloNative.h中包含了java方法的声明,只需在Native代码中包含该头文件并实现方法。
/*第3步*/
#include"HelloNative.h"
#include<stdio.h>
JNIEXPORT void JNICALL Java_HelloNative_greeting(JNIEnv* env,jclass cl)
{
printf("Hello Native World\n");
}
方法的第一个参数,JNIEnv* 为Java Native Interface Environmen,译作Java本地接口环境,是指向可用JNI函数表的接口指针。函数表的每一个成员指向一个JNI函数,Native方法通过JNI函数访问JVM中的数据结构。JNIEnv指针在jni.h中实现。
C调用JNI函数时,需要先对JNIEnv指针解引用
return (*env)->NewStringUTF(env,”Hello Native World\n”)
Cpp调用JNI函数时,不需要先对JNIEnv指针解引用
return env->NewStringUTF(“Hello Native World\n”)
方法的第二个参数,当Native方法为静态方法时,为Jclass,是HelloNative类的Java对象引用;当Native方法为非静态方法时,为Jobject,是HelloNative类实例的Java对象引用。
/*第4步*/
/*将C代码编译到一个动态装载库中,具体方法依赖于编译器*/
/*linux 下的 Gnu C编译器*/
gcc -fPIC -I jdk/include -I jdk/include/linux -shared -o libHelloNative.so Hellonative.c
/*Windows下的微软编译器*/
cl -I jdk\include -I jdk\include\win32 -LD HelloNative.c -FeHelloNative.dll
/*第5步*/
class HelloNativeTest
{
public static void main(String args[])
{
HelloNative.greeting();
}
}
static
{
System.loadLibrary("HelloNative");
}
java.lang.System类提供了两个静态方法用于在运行时加载共享库,void load(String filename)和void loadLibrary(String libname)。给loadLibrary()传的参数只是库名,JVM根据操作系统加上必要前后缀。
数据类型转换
Java数据类型分为基本数据类型和引用数据类型,JNI也区别对待这两种类型。JNI数据类型和方法是用于传入、传出Native代码的。如java中的函数public static native void exp(int x,double x ),需要两个参数x,y。在Native中实现时,x类型为jint,y类型为jdouble,函数为JNIEXPORT void JNICALL Java_HelloNative_exp(JNIEnv* env,jclass cl,jint x,jdouble y)。如果不用于互通用途,用Native原生的类型(int,double)、方法即可。
基本数据类型
引用数据类型
JNI API
引用类型以不透明的引用方式传递给原生代码,因此不能直接使用和修改,JNI提供了一组API操作引用类型,这些API通过JNIEnv接口指针提供给原生函数。
字符串
JNI将字符串视为引用类型,针对Unicode和UTF-8编码格式的字符串,提供了两组函数进行处理。
Unicode | UTF-8 |
NewString | NewStringUTF |
GetStringChars | GetStringUTFChars |
ReleaseStringChars | ReleaseStringUTFChars |
1.用给定的C字符串创建Java字符串
jstring javastring;
javaString = (*env)->NewStringUTF(env,"Hello World!");
2.将Java字符串转换为C字符串
const jbyte* str;
str = (*env)->GetStringUTFChars(env,javastring);
3.释放字符串
(*env)->ReleaseStringUTFChars(env,javaString,str)
数组操作
1.创建数组,使用New<Type> Array创建数组,<Type>为Int,Char,Double等。
jintArray javaArray;
javaArray = (*env)->NewIntArray(env,10);
2.访问数组,JNI提供两种方式访问Java数组的元素。
2.1 使用Get<Type>ArrayRegion函数将Java数组复制为C数组,如此可以像对普通C数组一样使用,之后再使用Set<Type>ArrayRegion函数将C数组还原为Java数组。
jint nativeArray[10];
(*env)->GetIntArrayRegion(env,javaArray,0,10,nativeArray);
(*env)->SetIntArrayRegion(env,javaArray,0,10,nativeArray);
2.2 让JNI提供直接指向数组元素的指针。使用Get<Type>ArrayElements获取指针。JNI要求操作完成后后立即使用Release<Type>ArrayElements释放数组,否则回内存溢出。
jint* nativeDirectoryArray;
nativeDirectoryArray = (*env)->GetIntArrayElements(env,javaArray);
(*env)->ReleaseIntArrayElements(env,javaArray,nativeDirectoryArray,0);
访问域
Java有实例域和静态域,JNI提供了访问两类域的方法。
3.1使用GetObjectClass或FindClass方法通过给定实例的class对象获取域ID。
jcalss clazz;
clazz = (*env)->GetObjectClass(env,instance);
3.2 获取实例域和静态域的域ID
jfieldID InstanceFieldID;
instanceFieldId = (*env)->GetFieldID(env,clazz,"instanceField","Ljava/lang/String;");
jfieldID staticFieldID;
staticFieldId = (*env)->GetStaticFieldID(env,clazz,"staticField","Ljava/lang/String;");
3.3使用Get<Type>Field和GetStatic<Type>Field获取域
jstring InstanceField;
instanceField = (*env)->GetField(env,clazz,instance,instanceField);
jstring staticField;
staticField = (*env)->GetStaticFieldID(env,clazz,staticField);
调用方法
/*获取实例方法ID*/
jmethodID instanceMethodId;
instanceMethodId = (*env)->GetMethodID(env,clazz,"instanceMethod","()Ljava/lang/String;");
/*调用实例方法ID*/
jstring instanceMethodResult;
staticMethodResult = (*env)->CallStaticStringMethod(env,clazz,"staticMethod","()Ljava/lang/String;");
/*获取静态方法ID*/
jmethodID instanceMethodId;
instanceMethodId = (*env)->GetMethodID(env,clazz,"instanceMethod","()Ljava/lang/String;");
/*调用静态方法ID*/
jstring staticMethodResult;
staticMethodResult = (*env)->CallStaticStringMethod(env,clazz,instanceMethodId);
参考书籍:《Java核心技术卷2 高级特性》
《Android C++高级编程使用NDK》
《Android高级开发实战-UI、NDK与安全》