4. JNI 数组操作与批量数据传递

JNI 数组操作与批量数据传递 学习卡片

JNI 数组访问的三种路径

Java 的 int[] 在 JNI 层对应 jintArray,它不保证与 C/C++ 的 int* 内存布局直接兼容。JNI 提供了三种访问路径,各自适用于不同场景:

  • GetIntArrayElements / ReleaseIntArrayElements:返回一个 jint* 指针,可能是 JVM 内部数组的直接地址,也可能是临时拷贝。适合需要随机读写、多次访问的场景,但必须成对释放,且释放时通过 mode 参数决定是否回写。
  • GetIntArrayRegion / SetIntArrayRegion:把指定区间的元素拷贝到调用者提供的 C 缓冲区,或从缓冲区写回 Java 数组。适合批量处理、只读或只写、以及需要避免长时间持有 JVM 内部指针的场景。
  • GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical:在“临界区”内直接获取数组指针,禁止在临界区内调用其他 JNI 函数或阻塞操作。适合对性能极度敏感且访问时间极短的场景。

下面这张图对比了三种路径的数据流向与关键约束:

Java 层 int[] data JNI 层 jintArray arr 路径 A:Elements 指针 GetIntArrayElements(arr, &isCopy) → jint* p(可能直接/拷贝) ReleaseIntArrayElements(arr, p, mode) 路径 B:Region 拷贝 GetIntArrayRegion(arr, 0, n, buf) → 拷贝到 C 缓冲区 buf[n] SetIntArrayRegion(arr, 0, n, buf) 路径 C:Critical 临界区 GetPrimitiveArrayCritical(arr, &isCopy) → jint* p(禁止其他 JNI 调用) ReleasePrimitiveArrayCritical(arr, p, mode) C/C++ 侧 jint* p / jint buf[n] 求和 / 原地修改 引用传递 回写 Java 数组 mode=0 回写并释放 SetIntArrayRegion 显式写回 路径 A:灵活但需成对释放,isCopy 可判断是否拷贝 路径 B:批量拷贝,无指针悬挂风险 路径 C:最快,但临界区极短

实现:数组求和与原地修改

下面实现两个 native 方法:sumArray 使用 GetIntArrayRegion 批量拷贝后求和;doubleArray 使用 GetIntArrayElements 获取指针后原地修改,并通过 mode=0 回写。

// native-lib.cpp
#include <jni.h>
#include <android/log.h>
#include <cstring>

#define LOG_TAG "JNIArray"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

extern "C" {

// 方式一:GetIntArrayRegion 批量拷贝求和
JNIEXPORT jint JNICALL
Java_com_example_ndkdemo_ArrayOps_sumArray(JNIEnv *env, jobject /* this */, jintArray arr) {
    if (arr == nullptr) return 0;

    jsize len = env->GetArrayLength(arr);
    if (len <= 0) return 0;

    // 在 C 堆上分配缓冲区,避免在 JNI 临界区中做耗时操作
    jint *buf = new jint[len];
    if (buf == nullptr) return 0;

    // 一次性把 Java 数组内容拷贝到 C 缓冲区
    env->GetIntArrayRegion(arr, 0, len, buf);

    jint sum = 0;
    for (jsize i = 0; i < len; ++i) {
        sum += buf[i];
    }

    delete[] buf;
    LOGI("sumArray: len=%d, sum=%d", len, sum);
    return sum;
}

// 方式二:GetIntArrayElements 原地修改
JNIEXPORT void JNICALL
Java_com_example_ndkdemo_ArrayOps_doubleArray(JNIEnv *env, jobject /* this */, jintArray arr) {
    if (arr == nullptr) return;

    jsize len = env->GetArrayLength(arr);
    if (len <= 0) return;

    jboolean isCopy = JNI_FALSE;
    // 获取指针,isCopy 用于判断是否发生了拷贝
    jint *p = env->GetIntArrayElements(arr, &isCopy);
    if (p == nullptr) return;

    LOGI("doubleArray: isCopy=%d", isCopy);

    for (jsize i = 0; i < len; ++i) {
        p[i] = p[i] * 2;
    }

    // mode=0:回写内容并释放缓冲区
    env->ReleaseIntArrayElements(arr, p, 0);
}

// 方式三:GetPrimitiveArrayCritical 高性能求和
JNIEXPORT jint JNICALL
Java_com_example_ndkdemo_ArrayOps_sumArrayCritical(JNIEnv *env, jobject /* this */, jintArray arr) {
    if (arr == nullptr) return 0;

    jsize len = env->GetArrayLength(arr);
    if (len <= 0) return 0;

    jboolean isCopy = JNI_FALSE;
    jint *p = (jint *) env->GetPrimitiveArrayCritical(arr, &isCopy);
    if (p == nullptr) return 0;

    jint sum = 0;
    for (jsize i = 0; i < len; ++i) {
        sum += p[i];
    }

    // 临界区内不做其他 JNI 调用,尽快释放
    env->ReleasePrimitiveArrayCritical(arr, p, JNI_ABORT);
    return sum;
}

} // extern "C"

Java 侧调用与验证

// ArrayOps.java
package com.example.ndkdemo;

public class ArrayOps {
    static {
        System.loadLibrary("native-lib");
    }

    public native int sumArray(int[] arr);
    public native void doubleArray(int[] arr);
    public native int sumArrayCritical(int[] arr);

    public static void main(String[] args) {
        ArrayOps ops = new ArrayOps();

        int[] data = {1, 2, 3, 4, 5};
        int sum = ops.sumArray(data);
        System.out.println("sumArray = " + sum); // 预期 15

        ops.doubleArray(data);
        System.out.println("doubleArray 后: " + java.util.Arrays.toString(data));
        // 预期 [2, 4, 6, 8, 10]

        int sum2 = ops.sumArrayCritical(data);
        System.out.println("sumArrayCritical = " + sum2); // 预期 30
    }
}
ReleaseIntArrayElements 的 mode 参数:
  • 0:把 C 缓冲区内容回写到 Java 数组,然后释放缓冲区。
  • JNI_COMMIT:回写内容,但不释放缓冲区(可继续使用指针)。
  • JNI_ABORT:不回写,直接释放缓冲区(丢弃修改)。

三种访问方式的性能对比

在 Android 设备上对长度为 100 万的 int[] 分别执行 100 次求和,记录耗时(示例数据,实际数值因设备而异):

访问方式关键接口是否可能拷贝相对耗时适用场景
Region 拷贝GetIntArrayRegion总是拷贝到 C 缓冲区1.0x(基准)批量只读/只写,无指针悬挂风险
Elements 指针GetIntArrayElements可能直接或拷贝0.6x ~ 0.9x需要随机读写、多次访问
Critical 临界区GetPrimitiveArrayCritical通常直接0.3x ~ 0.5x极短时间内的密集计算
Critical 临界区的限制:在 GetPrimitiveArrayCritical 与 ReleasePrimitiveArrayCritical 之间,不能调用其他 JNI 函数、不能阻塞、不能分配 Java 对象。否则可能导致死锁或 JVM 崩溃。临界区应尽可能短。

常见问题与排查

  • 数组内容未修改:检查 ReleaseIntArrayElements 的 mode 是否为 JNI_ABORT,或是否忘记调用 Release。
  • 程序崩溃:检查 GetIntArrayElements 返回的指针是否为 nullptr,以及是否在 Release 后继续使用该指针。
  • 性能不达预期:用 isCopy 判断是否发生了拷贝;若频繁拷贝,考虑改用 Critical 或减少跨 JNI 调用次数。
  • GetArrayLength 返回负数:说明传入的 jintArray 为 nullptr 或非法引用,需在 Java 侧检查参数。