Android JNI开发系列(七)访问数组

JNI访问数组

JNI 中的数组分为基本类型数组和对象数组,它们的处理方式是不一样的,基本类型数组中的所有元素都是 JNI 的基本数据类型,可以直接访问。而对象数组中的所有元素是一个类的实例或其它数组的引用,和字符串操作一样,不能直接访问 Java 传递给 JNI 层的数组,必须选择合适的 JNI 函数来访问和设置 Java 层的数组对象。

访问基本数据类型数组

//
// Created by Peng Cai on 2018/10/10.
//
#include <jni.h>
#include <stdlib.h>
#include <string.h>

JNIEXPORT jint JNICALL
Java_org_professor_jni_bean_Student_sum(JNIEnv *env, jobject instance, jintArray stuScore_) {

    //GetIntArrayElements 第三个参数表示返回的数组指针是原始数组,
    // 还是拷贝原始数据到临时缓冲区的指针,如果是 JNI_TRUE:表示临时缓冲区数组指针,
    // JNI_FALSE:表示临时原始数组指针。开发当中,我们并不关心它从哪里返回的数组指针,
    // 这个参数填 NULL 即可,但在获取到的指针必须做校验,因为当原始数据在内存当中不是连续存放的情况下,
    // JVM 会复制所有原始数据到一个临时缓冲区,并返回这个临时缓冲区的指针。
    // 有可能在申请开辟临时缓冲区内存空间时,会内存不足导致申请失败,这时会返回 NULL。

    //NULL 相当于 JNI_FALSE 代表不拷贝数组中的内容到缓冲区
    //JNI_TRUE 拷贝数组中的内容到缓冲区
    //*stuScore 指针指向一个int 类型数组
    //C语言的内存有程序员来管理也就是说 手动手动分配与释放
    //调用该函数式最安全的 当GC扫描到stuScore_该对象,会给该对象加锁,本地方法会处于阻塞状态(block)

    //可能数组中的元素在内存中是不连续的,JVM可能会复制所有原始数据到缓冲区,然后返回这个缓冲区的指针
    jint *stuScore = (*env)->GetIntArrayElements(env, stuScore_, NULL);
    if(stuScore == NULL){
        return 0; //JVM复制原始数据到缓冲区失败
    }

    int sum = 0;
    int length = (*env)->GetArrayLength(env, stuScore);
    for (int i = 0; i < length; ++i) {
        sum += stuScore[i];
    }
    //释放stuScore 指向的缓冲区
    (*env)->ReleaseIntArrayElements(env, stuScore_, stuScore, 0);
    return sum;
}

//这种写法传递数组元素非常少时候效率高
JNIEXPORT jfloat JNICALL
Java_org_professor_jni_bean_Student_average(JNIEnv *env, jobject instance, jfloatArray stuScore_) {
//    jfloat *stuScore = (*env)->GetFloatArrayElements(env, stuScore_, NULL);

    jsize length = (*env)->GetArrayLength(env, stuScore_);
    //创建数组
    float *stuScoreTmp = (float *) malloc(sizeof(jfloat) * length); //申请缓冲区
    memset(stuScoreTmp,0, sizeof(jfloat)*length);//初始化缓冲区
    (*env)->GetFloatArrayRegion(env, stuScore_, 0, length, stuScoreTmp); //拷贝Java数组中的所有元素到缓冲区中
    int sum = 0;
    for (int i = 0; i < length; i++) {
        sum += *(stuScoreTmp + i);
    }

    jfloat ave = sum / length;
    free(stuScoreTmp);// 释放存储数组元素的缓冲区
//    (*env)->ReleaseFloatArrayElements(env, stuScore_, stuScore, 0);
    return ave;
}

JNIEXPORT jfloat JNICALL
Java_org_professor_jni_bean_Student_ave(JNIEnv *env, jobject instance, jfloatArray stuScore_) {
    //(*GetPrimitiveArrayCritical)(JNIEnv*, jarray, jboolean*);
    //直接获取一个指针获取原始数组 调用该函数会暂停GC线程,不能调用其他线程的阻塞或者等待式函数(wait notify)
    jfloat *stuScore = (*env)->GetPrimitiveArrayCritical(env, stuScore_, NULL);
    jsize length = (*env)->GetArrayLength(env, stuScore_);
    float sum = 0;
    for (int i = 0; i < length; i++) {
        sum += *(stuScore + i);
    }

    float ave = sum / length;
    (*env)->ReleasePrimitiveArrayCritical(env, stuScore_, stuScore, 0); //释放需要与上面对应
    return ave;
}

在 Java 中创建的对象全都由 GC(垃圾回收器)自动回收,不需要像 C/C++ 一样需要程序员自己管理内存。GC 会实时扫描所有创建的对象是否还有引用,如果没有引用则会立即清理掉。当我们创建一个像 int 数组对象的时候,当我们在本地代码想去访问时,发现这个对象正被 GC 线程占用了,这时本地代码会一直处于阻塞状态,直到等待 GC 释放这个对象的锁之后才能继续访问。为了避免这种现象的发生,JNI 提供了 Get/ReleasePrimitiveArrayCritical这对函数,本地代码在访问数组对象时会暂停 GC 线程。不过使用这对函数也有个限制,在 Get/ReleasePrimitiveArrayCritical 这两个函数期间不能调用任何会让线程阻塞或等待 JVM 中其它线程的本地函数或JNI函数,和处理字符串的 Get/ReleaseStringCritical 函数限制一样。这对函数和 GetIntArrayElements 函数一样,返回的是数组元素的指针。

小结

  • 对于小量的、固定大小的数组,应该选择 Get/SetArrayRegion 函数来操作数组元素是效率最高的。因为这对函数要求提前分配一个 C 临时缓冲区来存储数组元素,你可以直接在 Stack(栈)上或用 malloc 在堆上来动态申请,当然在栈上申请是最快的。有童鞋可能会认为,访问数组元素还需要将原始数据全部拷贝一份到临时缓冲区才能访问而觉得效率低?我想告诉你的是,像这种复制少量数组元素的代价是很小的,几乎可以忽略。这对函数的另外一个优点就是,允许你传入一个开始索引和长度来实现对子数组元素的访问和操作(SetArrayRegion函数可以修改数组),不过传入的索引和长度不要越界,函数会进行检查,如果越界了会抛出 ArrayIndexOutOfBoundsException 异常。
  • 如果不想预先分配 C 缓冲区,并且原始数组长度也不确定,而本地代码又不想在获取数组元素指针时被阻塞的话,使用 Get/ReleasePrimitiveArrayCritical 函数对,就像 Get/ReleaseStringCritical 函数对一样,使用这对函数要非常小心,以免死锁。
  • Get/ReleaseArrayElements 系列函数永远是安全的,JVM 会选择性的返回一个指针,这个指针可能指向原始数据,也可能指向原始数据的复制。

访问对象数组

JNI 提供了两个函数来访问对象数组,GetObjectArrayElement 返回数组中指定位置的元素,SetObjectArrayElement 修改数组中指定位置的元素。与基本类型不同的是,我们不能一次得到数据中的所有对象元素或者一次复制多个对象元素到缓冲区。因为字符串和数组都是引用类型,只能通过 Get/SetObjectArrayElement 这样的 JNI 函数来访问字符串数组或者数组中的数组元素

JNIEXPORT jobjectArray JNICALL
Java_org_professor_jni_bean_Student_initInt2DArray(JNIEnv *env, jobject instance, jint size) {

    jobjectArray result;
    jclass clsIntArray;
    jint i, j;
    // 1.获得一个int型二维数组类的引用
    clsIntArray = (*env)->FindClass(env, "[I");
    if (clsIntArray == NULL) {
        return NULL;
    }
    // 2.创建一个数组对象(里面每个元素用clsIntArray表示)
    result = (*env)->NewObjectArray(env, size, clsIntArray, NULL);
    if (result == NULL) {
        return NULL;
    }

    // 3.为数组元素赋值
    for (i = 0; i < size; ++i) {
        jint buff[256];
        jintArray intArr = (*env)->NewIntArray(env, size);
        if (intArr == NULL) {
            return NULL;
        }
        for (j = 0; j < size; j++) {
            buff[j] = i + j;
        }
        (*env)->SetIntArrayRegion(env, intArr, 0, size, buff);
        (*env)->SetObjectArrayElement(env, result, i, intArr);
        (*env)->DeleteLocalRef(env, intArr);
    }

    return result;
}

本地函数 initInt2DArray 首先调用 JNI 函数 FindClass 获得一个 int 型的二维数组类的引用,传递给FindClass 的参数”[I”是 JNI class descript(JNI 类型描述符,后面为详细介绍),它对应着 JVM 中的int[]类型。如果 int[]类加载失败的话,FindClass 会返回 NULL,然后抛出一个java.lang.NoClassDefFoundError: [I异常

接下来,NewObjectArray 创建一个新的数组,这个数组里面的元素类型用 intArrCls(int[])类型来表示。函数NewObjectArray 只能分配第一维,JVM 没有与多维数组相对应的数据结构,JNI 也没有提供类似的函数来创建二维数组。由于 JNI 中的二维数组直接操作的是 JVM 中的数据结构,相比 JAVA 和 C/C++创建二维数组要复杂很多。给二维数组设置数据的方式也非常直接,首先用 NewIntArray 创建一个 JNI 的 int 数组,并为每个数组元素分配空间,然后用 SetIntArrayRegionbuff[]缓冲中的内容复制到新分配的一维数组中去,最后在外层循环中依次将 int[]数组赋值到 jobjectArray 数组中,一维数组中套一维数组,就形成了一个所谓的二维数组。

另外,为了避免在循环内创建大量的 JNI 局部引用,造成 JNI 引用表溢出,所以在外层循环中每次都要调用DeleteLocalRef 将新创建的 jintArray 引用从引用表中移除。在 JNI 中,只有 jobject 以及子类属于引用变量,会占用引用表的空间,jint,jfloat,jboolean 等都是基本类型变量,不会占用引用表空间,即不需要释放。引用表最大空间为 512 个,如果超出这个范围,JVM 就会挂掉。

    原文作者:移动开发
    原文地址: https://my.oschina.net/caipeng/blog/2245420
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞