从JNI函数动态注册进阶一文可知,JNI把Java类型分为两类来处理,一类是基本类型,另一类是引用类型,而且引用类型都是用jobject
类型表示的,如下图所示
从这个图中可以看出,引用类型中有一种比较特殊的类型--数组,在JNI层中有好多个类型相对应,例如int[]
对应于jintArray
,String[]
对应于jobjectArray
。
从上图中其实还可以看到的一点是,JNI中对数组的处理也分为基本类型和引用类型。由于篇幅关系,本文只讲述JNI中基本类型数组的操作,下一篇文章讲述引用类型数组的操作。
例子
现在假设有一个Java类的native
方法
public class ArrayTest {
static {
System.loadLibrary("array_jni");
}
public native void handleArray(int[] a);
public static void main(String[] args) {
int[] a = {1, 2, 3};
new ArrayTest().handleArray(a);
}
}
并且再次假设在JNI层的实现函数如下
static void com_uni_ndkdemo_ArrayTest_handleArray(JNIEnv *env, jobject *thiz, jintArray array)
{
}
如果不了解JNI函数是如何注册的,可以参考不使用IDE做一次JNI开发, JNI函数动态注册 和 JNI函数动态注册进阶。
在com_uni_ndkdemo_ArrayTest_handleArray
中将会实现对基本类型数组的操作。
Get<PrimitiveType>ArrayElements
Get<PrimitiveType>ArrayElements
函数返回一个基本类型的数组,其中<PrimitiveType>
表示基本类型,例为GetIntArrayElements
是获取int
类型数组。
NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env,
ArrayType array, jboolean *isCopy);
首先解释下参数
ArrayType array
: Java的数组对象jboolean *iscopy
: 如果isCopy
不为NULL
,当返回指针指向的是原数组的拷贝时,*isCopy
值为JNI_TRUE
,如果返回的指针指向原数组,那么*isCopy
的值为JNI_FALSE
。
这个通用函数有两个类型需要替换,一个是返回类型NativeType
表示一个JNI类型,一个是ArrayType
表示JNI的数组类型。怎么替换呢?举个例子吧,如果ArrayType array
是Java的int[]
类型对象,那么ArrayType
就是Javaint[]
类型在JNI中的对应类型,也就是jintArray
。函数的返回值是一个指针,很显然int[]
在JNI中肯定是一个jint
的指针,因此NativeType
就是jint
。所以,对于Java的int[]
对象,函数原型如下
jint * GetIntArrayElements(JNIEnv *env, jintArray array, jboolean *isCopy);
GetIntArrayElements
返回一个jint
类型的指针(指针可能为NULL
),可能指向Java的原始数组,也可能指向一个拷贝的数组,这个取决于虚拟机,但是我们可以通过最后一个参数的值来判断是否有拷贝发生。
如果函数返回的指针指向原数组,那么所有的修改都会在原数组上进行,这与Java的方式是一致的。而如果函数返回的指针指向原始数组的拷贝,那么所有的修改都仅仅是在拷贝上进行的,原始数组不受影响。
然而是否发生拷贝并不是我们能决定的,我们能决定的是,当拷贝发生了,我们能够确保所有在拷贝上的修改都能写回到原始数组上,这就是Release<PrimitiveType>ArrayElements
函数的作用。
Release<PrimitiveType>ArrayElements
void Release<PrimitiveType>ArrayElements(JNIEnv *env,
ArrayType array, NativeType *elems, jint mode);
参数
ArrayType array
: Java数组对象NativeType * elems
: 指向JNI类型的指针,是通过Get<PrimitiveType>ArrayElements
函数获取的jint mode
的值有以下几种- 0 : 把拷贝数组的修改写回原数组,并释放
NativeType *elems
缓存。 JNI_COMMIT
: 把拷贝数组的修改写回原数组,但是并不释放NativeType *elems
缓存。JNI_ABORT
: 释放NativeType *elems
缓存,但并不把拷贝数组的修改写回原数组。
- 0 : 把拷贝数组的修改写回原数组,并释放
关于
mode
参数,如果Get<PrimitiveType>ArrayElements
函数不发生拷贝,mode
参数就没有任何影响。
Release<PrimitiveType>ArrayElements
函数其实是通知虚拟机NativeType *elems
指向的数组不会再被访问,虚拟机会根据参数mode
的值决定是否释放本地数组,以及是否把修改写回到原数组。
Release<PrimitiveType>ArrayElements
是一个通用的写法,举个具体的例子吧,当处理的是Java的int[]
对象时,函数原型如下
void ReleaseIntArrayElements(JNIEnv *env,
jintArray array, jint *elems, jint mode);
读者可根据前面所讲的类型替换原理来理解这里的类型替换,后面遇到这样的类型替换也只会给出某个具体的例子,并不会讲解如何替换。
实战
static void com_uni_ndkdemo_ArrayTest_handleArray(JNIEnv *env, jobject *thiz, jintArray array)
{
// 1. 获取数组的长度
jsize length = env->GetArrayLength(intArr);
// 2. 获取本地数组
jint *native_int_array = env->GetIntArrayElements(intArr, NULL);
// 3. 操作本地数组
for (int i = 0; i < length; i++)
{
native_int_array[i] += 100;
}
// 4. 释放本地数组
env->ReleaseIntArrayElements(intArr, native_int_array, 0);
}
调用GetIntArrayElements
函数的时候,第二个参数传入了NULL
,因此后面无法判断是否生成了数组的拷贝,然而在这里并不关心这个问题,因为在调用ReleaseIntArrayElements
函数的时候,第三个参数的值为0
,修改一定会应用到原始数组上。
调用GetIntArrayElements
函数返回的本地数组,在函数返回前也没有进行手动释放,这是因为调用ReleaseIntArrayElements
传入的第三个参数为0
,本地数组会被自动释放。
Get<PrimitiveType>ArrayRegion
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array,
jsize start, jsize len, NativeType *buf);
参数
ArrayType array
: Java数组jsize start
: 起始数组的索引jsize len
: 需要拷贝的长度NativeType * buf
: 本地缓冲区
Get<PrimitiveType>ArrayRegion
函数的效果是拷贝数组ArrayType array
的一部分到缓存NativeType *buf
中,这一部分的起始位置是start
,长度为len
。
如果处理的是int[]
类型,对应函数原型如下
void GetIntArrayRegion(JNIEnv *env, jintArray array, jsize start,
jsize len, jint *buf);
我们应该注意到了,Get<PrimitiveType>ArrayRegion
函数的效果是拷贝,那么自然而然有一个问题摆在眼前,那就是如果你关心拷贝数组上的修改需要写会到原始数组上,可以调用Set<PrimitiveType>ArrayRegion
,而如果你并不关心这个问题,可能什么也不用做(如果本地缓存是动态分配的,需要手动释放)。
Set<PrimitiveType>ArrayRegion
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array,
jsize start, jsize len, const NativeType *buf);
参数
ArrayType array
: Java数组对象jsize start
: 拷贝的起始索引jsize len
: 拷贝的长度NativeType *buf
: 拷贝的数据源
Set<PrimitiveType>ArrayRegion
把缓存buf
写回到原数组array
中,起始位置为start
,长度为len
。
参数
start
和len
都是对于原是数组array
来说的。
实战
static void com_uni_ndkdemo_ArrayTest_handleArray(JNIEnv *env, jobject *thiz, jintArray array)
{
// 获取数组长度
const jsize length = env->GetArrayLength(array);
// 在栈上创建缓冲区
jint buff[length - 1];
// 获取原始数组最后length -1个数组的拷贝
env->GetIntArrayRegion(array, 1, length - 1, buff);
// 修改拷贝数组
for (int i = 0; i < length - 1; ++i)
{
buff[i] += 100;
}
// 把拷贝数组的修改写会到原始数组中
env->SetIntArrayRegion(array, 1, length - 1, buff);
}
在这里例子中,缓冲区jint buff[length - 1]
是在栈上创建,因此在函数返回后会自动释放内存,而如果在堆上创建缓冲区,例如使用malloc
函数,那么在函数返回前需要手动释放内存,否则会造成内存泄露(C语言基本认知)。
当使用SetIntArrayRegion
把修改写回到原是数组后,在Java层是可以看到数组的改变的,大家可以自己打印看看,我这里就不演示了。
GetPrimitiveArrayCritical & ReleasePrimitiveArrayCritical
void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
这两个函数与Get/Release<primitivetype>ArrayElements
函数的使用方式是一样的,虚拟机可能返回一个指向原数组的指针,也可能返回一个指向拷贝数组的指针。
但是这两个函数在使用的时候有非常大的限制。这两个函数是要成对出现的,而且在调用这两个函数之间,不能调用可能导致当前线程阻塞并且等待其它线程的JNI函数或系统调用。但是这种限制也带来一些好处,它使本地代码更容易获取一个非拷贝版本的数组的指针,虚拟机也可能暂时禁用垃圾回收功能。禁用垃圾回收功能可以让本地代码执行更快,毕竟不会暂停本地线程,但是呢,短暂禁用垃圾回收功能对系统垃圾回收功能造成影响,可以说利与弊相辅相成。
在实际中使用这两个函数,需要斟酌斟酌,权衡利弊。
实战
static jlong com_uni_ndkdemo_ArrayTest_handleArray(JNIEnv *env, jobject thiz, jintArray array)
{
// 1. 获取数组的大小
jsize length = env->GetArrayLength(array);
// 2. 获取数组的指针
jint * a = (jint *) env->GetPrimitiveArrayCritical(array, NULL);
if (a == NULL) {
return 0;
}
// 3. 操作数组(不要进行耗时的JNI函数调用或者系统调用)
for (int i = 0; i < length; i++) {
a[i] += 100;
}
// 4. 释放本地数组,修改写回原始数组
env->ReleasePrimitiveArrayCritical(array, a, 0);
}
使用Get<Primitive>ArrayCritical
和Release<Primitive>ArrayCritical
这两个函数的之间,注意不要调用耗时的JNI函数或者系统调用,因为毕竟虚拟机有可能暂时禁用垃圾回收功能,如果进行耗时操作,就可能影响垃圾回收功能。
New<PrimitiveType>Array
前面的例子都是在JNI层处理Java层传过来的基本类型数组,也可以在JNI层创建基本类型数组并返回给Java层,使用的就是New<PrimitiveType>Array
函数
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
例如,如果要返回一个int[]
对象给Java层,那么New<PrimitiveType>Array
函数原型如下
jintArray NewIntArray(JNIEnv *env, jsize length);
那么,在创建完Java基本类型数组后,如何给每个元素赋值呢?当然就是用前面讲过的函数。
实战
假设Java类有一个返回int[]
的方法
public class ArrayTest {
static {
System.loadLibrary("array_jni");
}
public native int[] getNativeArray();
}
经过动态注册后,在JNI层的实现函数如下
static jintArray com_uni_ndkdemo_ArrayTest_getNativeArray(JNIEnv * env, jobject thiz)
{
// 1. 创建一个Java的int[]
jintArray array = env->NewIntArray(2);
// 2. 获取数组指针
jint *c_array = env->GetIntArrayElements(array, NULL);
// 3. 操作数组元素
c_array[0] = 110;
c_array[1] = 120;
// 4. 把修改写回原数组并且释放本地数组
env->ReleaseIntArrayElements(array, c_array, 0);
return array;
}
最关键的一步就是第一步,创建一个Java层的基本类型数组,剩下的事情就是在JNI层处理这个数组,很显然这个事情前面都讲过。
总结
在操作基本类型数组方面呢,有三种方式,Get/Release<PrimitiveType>ArrayElements
,Get/Set<PrimitiveType>ArrayRegion
以及 Get/Release<Primitive>ArrayCritical
。它们各有千秋,这里就总结下。
如果操作整个数组,并且要把修改引用到原数组,那么Get/Release<PrimitiveType>ArrayElements
是首选,如果更需要执行速度,那么可以选择Get/Release<Primitive>ArrayCritical
。
如果不需要把修改应用到原数组上,那么Get/Set<PrimitiveType>ArrayRegion
是首选,毕竟Get/Release<PrimitiveType>ArrayElements
和Get/Release<Primitive>ArrayCritical
对于是否生成数组的拷贝具有不确定性,需要加入代码进行判断。
预告
后面的文章就会讲解JNI的关于引用类型数组的操作,这个稍微就有点复杂,如果引用类型是字符串类型,那就又是另外一种说法了。不过在此之前,希望大家好好理解我前面写的文章,因为这是一环套一环的,基本功丢了,就无从谈起上层建筑了。