4. JNI 数组操作与批量数据传递
JNI 数组访问的三种路径
Java 的 int[] 在 JNI 层对应 jintArray,它不保证与 C/C++ 的 int* 内存布局直接兼容。JNI 提供了三种访问路径,各自适用于不同场景:
- GetIntArrayElements / ReleaseIntArrayElements:返回一个
jint*指针,可能是 JVM 内部数组的直接地址,也可能是临时拷贝。适合需要随机读写、多次访问的场景,但必须成对释放,且释放时通过mode参数决定是否回写。 - GetIntArrayRegion / SetIntArrayRegion:把指定区间的元素拷贝到调用者提供的 C 缓冲区,或从缓冲区写回 Java 数组。适合批量处理、只读或只写、以及需要避免长时间持有 JVM 内部指针的场景。
- GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical:在“临界区”内直接获取数组指针,禁止在临界区内调用其他 JNI 函数或阻塞操作。适合对性能极度敏感且访问时间极短的场景。
下面这张图对比了三种路径的数据流向与关键约束:
实现:数组求和与原地修改
下面实现两个 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 侧检查参数。