第3章 JNI基础数据类型:基本类型映射与数组操作

好,咱们今天聊聊JNI里最基础的东西——数据类型映射。说实话,很多新手一开始就被Java和C++之间怎么传数据搞懵了。我当年第一次写JNI时,就因为一个int传过去变成了负数,排查了半天。嗯,其实就是符号位的问题。

3.1 基本类型映射:int/float/boolean

Java和C++的基本类型,在JNI层有直接的对应关系。说白了,就是一一映射,但有几个坑要注意。

Java类型 JNI类型 C/C++类型 大小(字节)
int jint int32_t 4
float jfloat float 4
boolean jboolean uint8_t 1
byte jbyte int8_t 1
char jchar uint16_t 2
short jshort int16_t 2
long jlong int64_t 8
double jdouble double 8

你看这个表,基本就是一一对应。但有个细节——jboolean在C++里是uint8_t,也就是无符号8位整数。Java的boolean只有true和false,但C++里你可以给它赋0或非0。我个人习惯是严格用JNI_TRUEJNI_FALSE这两个宏,避免歧义。

核心原则:JNI基本类型在C++侧就是原生类型,直接当普通变量用就行。不需要释放,不需要管理生命周期。

3.2 String与jstring转换

字符串这块,是JNI里最容易出问题的地方。Java的String是Unicode编码,C++的char*通常是UTF-8或ASCII。怎么转?JNI提供了两套API。

3.2.1 从jstring获取C字符串

// Java层
public native String nativeProcessString(String input);

// C++层
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_NativeLib_nativeProcessString(
    JNIEnv* env, jobject thiz, jstring input) {
    
    // 方式一:获取UTF-8字符串
    const char* utfStr = env->GetStringUTFChars(input, nullptr);
    if (utfStr == nullptr) {
        return nullptr;  // 内存不足
    }
    
    // 使用utfStr...
    std::string result = process(utfStr);
    
    // 必须释放!
    env->ReleaseStringUTFChars(input, utfStr);
    
    // 方式二:获取Unicode字符串(jchar数组)
    jsize len = env->GetStringLength(input);
    const jchar* unicodeStr = env->GetStringChars(input, nullptr);
    // 使用...
    env->ReleaseStringChars(input, unicodeStr);
    
    return env->NewStringUTF(result.c_str());
}

我曾经踩过的坑:GetStringUTFChars返回的指针,用完一定要Release!否则会造成内存泄漏。而且这个泄漏很难查,因为JNI的引用是全局的,不会随函数结束自动释放。我有个项目跑了三天,内存涨了2个G,最后定位到就是这里。

3.2.2 创建Java字符串

从C++返回字符串给Java,用NewStringUTFNewString。注意:NewStringUTF要求传入的是修改过的UTF-8格式,不是标准的UTF-8。区别在于:空字符\0会被编码成两字节,而标准UTF-8是单字节。

// 创建字符串
jstring jstr = env->NewStringUTF("Hello from C++!");
// 或者从std::string创建
std::string msg = "你好,世界";
jstring jmsg = env->NewStringUTF(msg.c_str());

小技巧:如果你频繁创建和释放jstring,可以考虑用NewStringUTF的返回值直接返回,JVM会管理它的生命周期。但如果你要在C++侧长期持有,记得用NewGlobalRef创建全局引用。

3.3 数组操作:jintArray

数组操作比基本类型复杂一些。你想想看,Java的数组在堆上,C++要操作它,得先拿到指针。JNI提供了两套方案:临界区模式拷贝模式

3.3.1 获取数组元素

// Java层
public native int[] nativeProcessArray(int[] input);

// C++层
extern "C" JNIEXPORT jintArray JNICALL
Java_com_example_NativeLib_nativeProcessArray(
    JNIEnv* env, jobject thiz, jintArray input) {
    
    // 获取数组长度
    jsize len = env->GetArrayLength(input);
    
    // 方式一:获取指针(可能触发拷贝或直接引用)
    jint* elements = env->GetIntArrayElements(input, nullptr);
    if (elements == nullptr) {
        return nullptr;
    }
    
    // 处理数据
    for (int i = 0; i < len; i++) {
        elements[i] *= 2;  // 每个元素翻倍
    }
    
    // 释放并同步回Java
    env->ReleaseIntArrayElements(input, elements, 0);
    // 第三个参数:0=拷贝回并释放缓冲区
    // JNI_COMMIT=拷贝回但不释放
    // JNI_ABORT=释放但不拷贝回
    
    // 方式二:使用临界区(避免拷贝)
    jint* critical = env->GetPrimitiveArrayCritical(input, nullptr);
    if (critical != nullptr) {
        // 临界区内不能调用其他JNI函数!
        memcpy(critical, newData, len * sizeof(jint));
        env->ReleasePrimitiveArrayCritical(input, critical, 0);
    }
    
    return input;  // 直接返回修改后的数组
}

关键区别:

  • GetIntArrayElements:可能拷贝,也可能直接返回指针。安全,但性能一般。
  • GetPrimitiveArrayCritical:返回直接指针,性能极高。但临界区内禁止调用任何其他JNI函数,否则可能死锁。

3.3.2 创建新数组

// 创建指定长度的int数组
jintArray result = env->NewIntArray(len);
if (result == nullptr) {
    return nullptr;  // 内存不足
}

// 填充数据
jint* temp = new jint[len];
for (int i = 0; i < len; i++) {
    temp[i] = i * 10;
}

// 将数据设置到Java数组
env->SetIntArrayRegion(result, 0, len, temp);

delete[] temp;
return result;

3.3.3 其他基本类型数组

操作方式完全一样,只是函数名不同:

Java数组类型 获取元素 释放元素 设置区域
int[] GetIntArrayElements ReleaseIntArrayElements SetIntArrayRegion
float[] GetFloatArrayElements ReleaseFloatArrayElements SetFloatArrayRegion
boolean[] GetBooleanArrayElements ReleaseBooleanArrayElements SetBooleanArrayRegion
byte[] GetByteArrayElements ReleaseByteArrayElements SetByteArrayRegion
short[] GetShortArrayElements ReleaseShortArrayElements SetShortArrayRegion
long[] GetLongArrayElements ReleaseLongArrayElements SetLongArrayRegion
double[] GetDoubleArrayElements ReleaseDoubleArrayElements SetDoubleArrayRegion

性能警告:如果你只是读取数组,不改写,用GetIntArrayElements时第三个参数传JNI_ABORT,这样释放时不会把数据拷贝回去,能省一次内存拷贝。我优化过一个图像处理模块,就靠这个把帧率从30fps提到了45fps。

3.4 实战经验总结

嗯,说了这么多,我总结几个核心要点:

  1. 基本类型直接传,不用管生命周期,用完就完。
  2. 字符串一定要Release,否则内存泄漏等着你。
  3. 数组操作优先用临界区,但注意临界区内别调其他JNI函数。
  4. 频繁调用的场景,考虑用NewGlobalRef缓存jclass和jmethodID,避免每次查找。

我记得有一次,一个同事在循环里每次都用FindClass查找同一个类,结果性能直接崩了。我帮他改成在JNI_OnLoad里缓存,速度提升了10倍。你想想看,JNI调用本身就有开销,再叠加重复查找,那肯定慢。

好了,这一章的内容就这些。下一章咱们聊聊更复杂的——对象和数组的嵌套操作,还有怎么在C++里调用Java方法。到时候会有更多实战技巧。