java调用本地方法--JNI访问基本类型数组
本篇结构:
- 简介
- 实例
一、简介
补充JNI基本类型数组访问实例。
对于基本数据类型数组,JNI 都有和 Java 相对应的结构,在使用起来和基本数据类型的使用类似。
JNI 提供了对应的转换函数:GetArrayElements、ReleaseArrayElements。
intArray = env->GetIntArrayElements(intArray_, NULL);
env->ReleaseIntArrayElements(intArray_, intArray, 0);
JNI 还提供了如下的函数:
-
GetTypeArrayRegion / SetTypeArrayRegion
将数组内容复制到 C 缓冲区内,或将缓冲区内的内容复制到数组上。 -
GetArrayLength
得到数组中的元素个数,也就是长度。 -
NewTypeArray
返回一个指定数据类型的数组,并且通过 SetTypeArrayRegion 来给指定类型数组赋值。 -
GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical
如同 String 中的操作一样,返回一个指定基础数据类型数组的直接指针,在这两个操作之间不能做任何阻塞的操作。
二、实例
2.1、编写Java类
public class IntArray {
// 在本地代码中求数组中所有元素的和
private native int sumArray(int[] arr);
public static void main(String[] args) {
IntArray p = new IntArray();
int[] arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
int sum = p.sumArray(arr);
System.out.println("sum = " + sum);
}
static {
System.loadLibrary("IntArray");
}
}
2.2、编译java类
javac IntArray.java
2.3、生成相关JNI方法的头文件
javah -d jnilib -jni IntArray
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class IntArray */
#ifndef _Included_IntArray
#define _Included_IntArray
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: IntArray
* Method: sumArray
* Signature: ([I)I
*/
JNIEXPORT jint JNICALL Java_IntArray_sumArray
(JNIEnv *, jobject, jintArray);
#ifdef __cplusplus
}
#endif
#endif
2.4、使用C/C++实现本地方法
// IntArray.c
#include "IntArray.h"
#include <string.h>
#include <stdlib.h>
JNIEXPORT jint JNICALL Java_IntArray_sumArray
(JNIEnv *env, jobject obj, jintArray j_array)
{
jint i, sum = 0;
jint *c_array;
jint arr_len;
//1. 获取数组长度
arr_len = (*env)->GetArrayLength(env,j_array);
//2. 根据数组长度和数组元素的数据类型申请存放java数组元素的缓冲区
c_array = (jint*)malloc(sizeof(jint) * arr_len);
//3. 初始化缓冲区
memset(c_array,0,sizeof(jint)*arr_len);
printf("arr_len = %d ", arr_len);
//4. 拷贝Java数组中的所有元素到缓冲区中
(*env)->GetIntArrayRegion(env,j_array,0,arr_len,c_array);
for (i = 0; i < arr_len; i++) {
sum += c_array[i]; //5. 累加数组元素的和
}
free(c_array); //6. 释放存储数组元素的缓冲区
return sum;
}
2.5、生成动态链接库
gcc -D_REENTRANT -fPIC -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -shared -o libIntArray.so IntArray.c
2.6、运行java
java -Djava.library.path=jnilib IntArray
2.7、解释
在Java中定义了一个sumArray的native方法,参数类型是int[],对应JNI中jintArray类型。
在本地代码中,首先通过JNI的GetArrayLength函数获取数组的长度,已知数组是jintArray类型,可以得出数组的元素类型是jint,然后根据数组的长度和数组元素类型,申请相应大小的缓冲区。如果缓冲区不大的话,当然也可以直接在栈上申请内存,那样效率更高,但是没那么灵活,因为Java数组的大小变了,本地代码也跟着修改。接着调用GetIntArrayRegion函数将Java数组中的所有元素拷贝到C缓冲区中,并累加数组中所有元素的和,最后释放存储java数组元素的C缓冲区,并返回计算结果。GetIntArrayRegion函数第1个参数是JNIEnv函数指针,第2个参数是Java数组对象,第3个参数是拷贝数组的开始索引,第4个参数是拷贝数组的长度,第5个参数是拷贝目的地。
在前面的例子当中,通过调用GetIntArrayRegion函数,将int数组中的所有元素拷贝到C临时缓冲区中,然后在本地代码中访问缓冲区中的元素来实现求和的计算,JNI还提供了一个和GetIntArrayRegion相对应的函SetIntArrayRegion,本地代码可以通过这个函数来修改所有基本数据类型数组的元素。
另外JNI还提供一系列直接获取数组元素指针的函数Get/Release<Type>ArrayElements,比如:GetIntArrayElements、ReleaseArrayElements、GetFloatArrayElements、ReleaseFloatArrayElements等。下面我们用这种方式重新实现计算数组元素的和。
JNIEXPORT jint JNICALL Java_IntArray_sumArray
(JNIEnv *env, jobject obj, jintArray j_array)
{
jint i, sum = 0;
jint *c_array;
jint arr_len;
// 可能数组中的元素在内存中是不连续的,JVM可能会复制所有原始数据到缓冲区,然后返回这个缓冲区的指针
c_array = (*env)->GetIntArrayElements(env,j_array,NULL);
if (c_array == NULL) {
return 0; // JVM复制原始数据到缓冲区失败
}
arr_len = (*env)->GetArrayLength(env,j_array);
printf("arr_len = %d\n", arr_len);
for (i = 0; i < arr_len; i++) {
sum += c_array[i];
}
(*env)->ReleaseIntArrayElements(env,j_array, c_array, 0); // 释放可能复制的缓冲区
return sum;
}
GetIntArrayElements第三个参数表示返回的数组指针是原始数组,还是拷贝原始数据到临时缓冲区的指针,如果是JNI_TRUE:表示临时缓冲区数组指针,JNI_FALSE:表示临时原始数组指针。开发当中,我们并不关心它从哪里返回的数组指针,这个参数填NULL即可,但在获取到的指针必须做校验,因为当原始数据在内存当中不是连续存放的情况下,JVM会复制所有原始数据到一个临时缓冲区,并返回这个临时缓冲区的指针。有可能在申请开辟临时缓冲区内存空间时,会内存不足导致申请失败,这时会返回NULL。
在Java中创建的对象全都由GC(垃圾回收器)自动回收,不需要像C/C++一样需要程序员自己管理内存。GC会实时扫描所有创建的对象是否还有引用,如果没有引用则会立即清理掉。当我们创建一个像int数组对象的时候,当我们在本地代码想去访问时,发现这个对象正被GC线程占用了,这时本地代码会一直处于阻塞状态,直到等待GC释放这个对象的锁之后才能继续访问。为了避免这种现象的发生,JNI提供了Get/ReleasePrimitiveArrayCritical这对函数,本地代码在访问数组对象时会暂停GC线程。不过使用这对函数也有个限制,在Get/ReleasePrimitiveArrayCritical这两个函数期间不能调用任何会让线程阻塞或等待JVM中其它线程的本地函数或JNI函数,和处理字符串的Get/ReleaseStringCritical函数限制一样。这对函数和GetIntArrayElements函数一样,返回的是数组元素的指针。
JNIEXPORT jint JNICALL Java_IntArray_sumArray
(JNIEnv *env, jobject obj, jintArray j_array)
{
jint i, sum = 0;
jint *c_array;
jint arr_len;
jboolean isCopy;
c_array = (*env)->GetPrimitiveArrayCritical(env,j_array,&isCopy);
printf("isCopy: %d \n", isCopy);
if (c_array == NULL) {
return 0;
}
arr_len = (*env)->GetArrayLength(env,j_array);
printf("arr_len = %d\n", arr_len);
for (i = 0; i < arr_len; i++) {
sum += c_array[i];
}
(*env)->ReleasePrimitiveArrayCritical(env, j_array, c_array, 0);
return sum;
}
2.8、总结
1、对于小量的、固定大小的数组,应该选择Get/SetArrayRegion函数来操作数组元素是效率最高的。因为这对函数要求提前分配一个C临时缓冲区来存储数组元素,可以直接在Stack(栈)上或用malloc在堆上来动态申请,当然在栈上申请是最快的。也许会有人有疑问,访问数组元素还需要将原始数据全部拷贝一份到临时缓冲区才能访问而觉得效率低?其实像这种复制少量数组元素的代价是很小的,几乎可以忽略。这对函数的另外一个优点就是,允许传入一个开始索引和长度来实现对子数组元素的访问和操作(SetArrayRegion函数可以修改数组),不过传入的索引和长度不要越界,函数会进行检查,如果越界了会抛出ArrayIndexOutOfBoundsException异常。
2、如果不想预先分配C缓冲区,并且原始数组长度也不确定,而本地代码又不想在获取数组元素指针时被阻塞的话,使用Get/ReleasePrimitiveArrayCritical函数对,就像Get/ReleaseStringCritical函数对一样,使用这对函数要非常小心,以免死锁。
3、Get/Release<type>ArrayElements系列函数永远是安全的,JVM会选择性的返回一个指针,这个指针可能指向原始数据,也可能指向原始数据的复制。