第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_TRUE和JNI_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,用NewStringUTF或NewString。注意: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 实战经验总结
嗯,说了这么多,我总结几个核心要点:
- 基本类型直接传,不用管生命周期,用完就完。
- 字符串一定要Release,否则内存泄漏等着你。
- 数组操作优先用临界区,但注意临界区内别调其他JNI函数。
- 频繁调用的场景,考虑用
NewGlobalRef缓存jclass和jmethodID,避免每次查找。
我记得有一次,一个同事在循环里每次都用FindClass查找同一个类,结果性能直接崩了。我帮他改成在JNI_OnLoad里缓存,速度提升了10倍。你想想看,JNI调用本身就有开销,再叠加重复查找,那肯定慢。
好了,这一章的内容就这些。下一章咱们聊聊更复杂的——对象和数组的嵌套操作,还有怎么在C++里调用Java方法。到时候会有更多实战技巧。