JNI基本类型数组操做

JNI函数动态注册进阶一文可知,JNI把Java类型分为两类来处理,一类是基本类型,另外一类是引用类型,并且引用类型都是用jobject类型表示的,以下图所示java

引用类型

从这个图中能够看出,引用类型中有一种比较特殊的类型--数组,在JNI层中有好多个类型相对应,例如int[]对应于jintArrayString[]对应于jobjectArrayc++

从上图中其实还能够看到的一点是,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层的实现函数以下bash

static void com_uni_ndkdemo_ArrayTest_handleArray(JNIEnv *env, jobject *thiz, jintArray array) {

}
复制代码

若是不了解JNI函数是如何注册的,能够参考不使用IDE作一次JNI开发, JNI函数动态注册JNI函数动态注册进阶函数

com_uni_ndkdemo_ArrayTest_handleArray中将会实现对基本类型数组的操做。post

Get<PrimitiveType>ArrayElements

Get<PrimitiveType>ArrayElements函数返回一个基本类型的数组,其中<PrimitiveType>表示基本类型,例为GetIntArrayElements是获取int类型数组。ui

NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env,
                        ArrayType array, jboolean *isCopy);
复制代码

首先解释下参数spa

  • 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缓存,但并不把拷贝数组的修改写回原数组。

关于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

参数startlen都是对于原是数组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>ArrayCriticalRelease<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>ArrayElementsGet/Set<PrimitiveType>ArrayRegion 以及 Get/Release<Primitive>ArrayCritical。它们各有千秋,这里就总结下。

若是操做整个数组,而且要把修改引用到原数组,那么Get/Release<PrimitiveType>ArrayElements是首选,若是更须要执行速度,那么能够选择Get/Release<Primitive>ArrayCritical

若是不须要把修改应用到原数组上,那么Get/Set<PrimitiveType>ArrayRegion是首选,毕竟Get/Release<PrimitiveType>ArrayElementsGet/Release<Primitive>ArrayCritical对因而否生成数组的拷贝具备不肯定性,须要加入代码进行判断。

预告

后面的文章就会讲解JNI的关于引用类型数组的操做,这个稍微就有点复杂,若是引用类型是字符串类型,那就又是另一种说法了。不过在此以前,但愿你们好好理解我前面写的文章,由于这是一环套一环的,基本功丢了,就无从谈起上层建筑了。

相关文章
相关标签/搜索