ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JNI基础数据类型全解析:从jboolean到jlong的常见坑

JNI基础数据类型全解析:从jboolean到jlong的常见坑 1. 从JNI的类型系统说起为什么第一课必须讲这个JNIJava Native Interface是Java生态里一个特别的存在。它让Java代码可以直接调用C/C写的动态库反过来C/C代码也能调用Java层的代码。十年前我做Android系统定制时第一次真正用上JNI后来转做后端中间件在Java服务里用JNA/JNI调一些底层算法库可以说JNI贯穿了我整个职业生涯的很多关键阶段。很多初学者一上来就急着写代码、调接口结果被jstring、jobjectArray、jfieldID这些东西搞得一头雾水。其实JNI的所有困难根源都在类型系统——你用什么类型接收Java层传过来的参数、用什么类型把数据传回Java层直接决定了后面一切调用的成败。这篇内容我会把JNI类型系统的全貌讲清楚重点聚焦基础数据类型这一块。内容偏向实战为什么JNI要定义一套自己的typedef这些类型和C/C原生类型之间怎么换算在CLion里搭环境时要注意哪些坑以及我在实际项目中踩过的那些看起来能编译、跑起来就崩的问题。如果你是下面这几种情况这篇文章值得读完正在学JNI刚开始接触javac -h和C/C代码生成对jni.h里的类型定义感到陌生在维护老项目遇到了UnsatisfiedLinkError、JNI参数错位或者native层读到乱码的问题想在CLion里配置一套完整的JNI开发调试环境不想再用命令行手动javac/编译的笨办法。JNI类型系统说白了就两件事一是Java和C/C之间的数据怎么“翻译”二是翻译过程中谁能保证字节数、符号、内存布局完全不出错。基础数据类型是整个JNI类型系统的地基地基不牢后面说什么jobject回调、局部引用管理都是在空中楼阁。2. 基础数据类型全解析jboolean到jdouble的每一个坑2.1 JNI为什么不用C语言的int、long先回答一个非常自然的疑问Java层有int、long、booleanC语言也有int、long为什么JNI不能直接用我在给团队做内部培训时经常举这个例子C标准里只规定了int最短16位、long最长不超过double但具体是多少位由编译器和平台决定。在Windows上是LLP64模型long是32位在Linux x86_64上是LP64模型long是64位。Java的int永远32位、long永远64位这是语言规范写死的事情。如果JNI直接映射同一个C函数在Windows下编译和Linux下编译对于long类型参数的二进制布局可能完全不同。老练的C程序员肯定会说用int32_t、int64_t这些stdint.h里的定长类型不就行了JNI思路一致但它比这个更进一步它直接在jni.h头文件里把类型全部typedef出来形成一套Java世界和原生世界之间的“契约语言”。这就是JNI类型系统存在的核心价值用一套固定的、平台无关的typedef屏蔽Java与C/C之间的类型差异。你看jni.h时会发现所有平台的JNI实现都约定同一个头文件、同一套类型名保证Java代码“一处编写、处处原生”。2.2 八大基础数据类型的完整映射表JNI的基础类型定义在jni.h的头部核心映射我整理成一张表Java类型JNI的C/C类型实际C语言底层定义占用字节数说明booleanjbooleanunsigned char1字节只约定0和1bytejbytesigned char1字节有符号8位charjcharunsigned short2字节Java的char是UTF-16编码单元不是ASCIIshortjshortshort2字节有符号16位intjintint4字节固定32位等价int32_tlongjlonglong long8字节固定64位等价int64_tfloatjfloatfloat4字节IEEE 754单精度doublejdoubledouble8字节IEEE 754双精度voidvoidvoid—仅用于方法返回类型这张表里最值得警惕的就是jboolean和jchar它们也是我见到的错误率最高的两个类型。jboolean底层是unsigned char而不是C里的bool。Java层的boolean语义上只有true/falsenative层用unsigned char只是为了严格保证1个字节的存储尺寸。C的bool标准虽然也通常是1字节但C标准的bool和这毕竟不是同一个东西所以JNI规范里特意定义了jboolean。实际调用时如果Java传了true你收到的是1传false收到0。但你在native层用jboolean接收后用if (value JNI_TRUE)判断这里的JNI_TRUE定义就是常量1。jchar是unsigned short2字节无符号。这里有个隐蔽的坑C/C里的char几乎都是1字节很多新手在C/C侧把JNI返回的jchar直接强转为char结果遇到中文或者4字节以上的Unicode码点时数据直接截断。Java的char是UTF-16 code unit你接到的jchar可能是代理对surrogate pair中的一半。如果你要做字符串处理应该走GetStringChars或者GetStringUTFChars而不是自己逐个转char。jlong的坑更偏向“习惯”在Windows上的C代码里long是32位但JNI的jlong明确是long long。如果你写成long接收jlong编译能过但高32位直接被丢掉返回的数据完全错乱。我见过不止一个同事在这种地方花了一整天才排查出来。2.3 类型映射背后的数字讲究为什么正好是这些字节长度有人会问Java的byte如果映射成signed char为什么不用int8_tJava的int为什么不用int32_t答案不是JNI老而是JNI要考虑C89/C99的老编译器兼容性。当年JNI规范定下来的时候C99的stdint.h还没有被广泛支持JNI在jni.h里自己用typedef定义了一套类型保证哪怕在非常老旧的C编译环境下也能编译通过。不过现代的jni.h里也大量配合使用stdint.h里的类型typedef unsigned char jboolean; typedef unsigned short jchar; typedef short jshort; typedef int jint; // 在特定的JNI实现里jlong也可能是 // typedef long long jlong;你去看JDK自带的jni.h有的版本里jint就是intjlong就是long long但这都无所谓因为JNI规范保证的是“字节宽度固定、符号固定”。真正重要的是你在自己的C/C代码里用等宽类型去接// 推荐做法使用定长类型 int32_t myJavaInt jint_value; int64_t myJavaLong jlong_value;这种写法的好处在于你的native代码逻辑是自文档化的明示了数据宽度未来跨平台移植时不会因为int、long在不同平台上的默认宽度不一致而出问题。2.4 为什么没有jbyteArray的基础类型对应物JNI区分“基础类型”和“引用类型”。八种基础类型对应Java的基本类型引用类型则对应数组、String、Class、Throwable以及所有Java对象。最容易混淆的是jbyteArray、jintArray这类东西——它们不是基础类型而是数组类型属于引用类型范畴。JNI设计时给每种基础类型都配了一个对应的Array类型但不建议把它们和基础类型混在一起记忆基础类型对应的JNI数组类型获取数据的JNI函数jbooleanjbooleanArrayGetBooleanArrayElementsjbytejbyteArrayGetByteArrayElementsjcharjcharArrayGetCharArrayElementsjshortjshortArrayGetShortArrayElementsjintjintArrayGetIntArrayElementsjlongjlongArrayGetLongArrayElementsjfloatjfloatArrayGetFloatArrayElementsjdoublejdoubleArrayGetDoubleArrayElements从这里能看出JNI的一贯思路基础类型是值传递的数组和对象是引用类型的需要JNI函数专门获取与释放。这篇着重讲基础类型但数组类型你一定会碰到尤其是byte数组在传输二进制数据时几乎避不开。后面的章节里我会详细展开数组和引用类型的细节。3. JNIEnV的取值与传值机制基础数据到底怎么跨语言流动3.1 基础类型按值传递这句话怎么理解熟悉C的人都知道函数参数有两种传递方式值传递和引用传递。JNI对于基础类型采用的就是值传递。Java层调用native方法时int参数直接复制一份原始值传到C函数栈上C函数返回jint时也直接返回值本身。这意味着native函数里对参数的修改不会影响Java层的原变量。这和Java本身的基本类型语义是一致的。理解这一点对调试很有用如果你在native层改了jint的值Java层没变不是JNI出bug了是你对值传递的预期错了。但是有个重要的例外如果你传入的是一个int[]数组jintArray哪怕每个元素是int整个数组在JNI层面是一个引用类型。你操作数组元素时必须通过GetIntArrayElements拿到C侧指针这时候你在C侧改数组元素如果调用了ReleaseIntArrayElements时用了JNI_COMMIT或0是可以把修改同步回Java数组的。所以“基础类型按值传递”只对单个标量成立对数组完全不成立。3.2 JNIEXPORT和JNIEXPORT的符号导出原理看JNI的native函数声明会有两个醒目的宏JNIEXPORT jint JNICALL Java_com_example_NativeLib_add(JNIEnv *env, jobject thiz, jint a, jint b);JNIEXPORT这个宏在Windows上是__declspec(dllexport)在Linux/Unix上为空或__attribute__((visibility(default)))JNICALL在Windows上是__stdcall调用约定在Linux等平台上为空。这段宏的作用是控制函数导出和调用约定。Windows下DLL的符号默认不导出如果没有JNIEXPORTJava层运行时就会报“找不到add符号”也就是UnsatisfiedLinkError。所以如果你在Windows上手动写JNI函数名千万别漏了这两个宏。在CMakeLists.txt里设置C_VISIBILITY_PRESET hidden时这两个宏的意义就更重要了。从JVM的角度看动态库加载后JVM会通过dlsymLinux或GetProcAddressWindows查找符号。如果找不到就报错。符号本身还必须是extern C的否则C编译器会做名字改编name manglingJVM按C符号去找就会扑空。这里我列一下最常见的三个native层找不到符号的场景漏了extern CWindows下漏了JNIEXPORT导致未导出用C写了函数重载名字被编译器改编每个都让你报错UnsatisfiedLinkError但报错位置和时机略有不同。实际开发中我建议所有JNI封装函数都按这个格式严格写全不要偷懒省宏。3.3 从Java层调用native方法时的完整数据流以最简单的add方法为例完整数据流是这样的Java层调用NativeLib.add(3, 5)JVM根据System.loadLibrary(native)加载动态库把符号解析注册到当前进程调用时JVM把JNIEnv指针和jobject或jclass看方法是否static压栈再把参数按JNI类型压栈native函数执行拿到a3、b5算出jint返回值返回值通过JNI约定传回Java层Java方法获得8。整个过程中JVM不关心你native函数内部怎么实现的它只关心入口符号约定是否匹配、参数栈布局是否匹配。这就是为什么类型语义这么重要——一旦某个参数类型对不上压栈/出栈的字节数不一样函数可能不立即崩溃直到下一次调用栈返回时才炸这种bug极其难查。我举一个我真实遇到过的例子。项目里有人把native方法签名里的long参数换成了int参数因为两边的代码不是同一个人写的Java层传的是longC层接的是jint。64位系统下long是8字节jint是4字节。JVM压栈了8字节C函数按4字节解析局部变量后面的参数全部错位。最终表现是函数能进能出但第二个参数永远是个垃圾值而且概率性崩溃。用valgrind一跑才发现栈被写坏了。所以Java和native层的类型签名必须严格一致这是JNI第一条军规。类型映射表不是参考建议是法律条文。4. CLion中配置JNI开发环境从零到一跑通第一个example4.1 为什么推荐CLion作为JNI开发IDECLion对JNI开发的支持在当前的IDE阵营里算是比较友好的一档。一方面JetBrains家的IDEA和CLion可以联动端到端调试Java调用native方法的场景可以直接在CLion的调试器里断点另一方面CLion原生支持CMake而JNI的C/C侧正好绝大多数都是用CMake构建的。相比Eclipse CDT那套老掉牙的组合CLion的环境配置要顺滑很多。我在开篇提到的“在CLion中配置jni环境”这个热搜词也确实反映了很多人的刚需。下面我给的配置流程基于Linux/macOS环境Windows步骤基本一致差异主要是JAVA_HOME路径和动态库后缀名Windows是.dllmacOS是.jnilib或.dylibLinux是.so。4.2 第一步准备JDK和CLion先用java -version确认你的JDK版本。JNI开发建议用JDK 8或更高的版本。然后记下你的JAVA_HOME路径# Linux/macOS echo $JAVA_HOME # 如果没有设置先找到java所在路径 which java # 然后推断JAVA_HOME一般是 /usr/lib/jvm/java-11-openjdk-amd64 之类一般CMakeLists.txt里需要这个路径来引入jni.h和jni_md.h。4.3 第二步创建Java端代码并生成头文件CLion虽然主打C/C但它也能混编。我习惯把Java文件放在项目的java_src目录下这样便于管理。新建一个Java类public class NativeLib { static { System.loadLibrary(native); } public native int add(int a, int b); public native String getMessage(); public static void main(String[] args) { NativeLib lib new NativeLib(); System.out.println(3 5 lib.add(3, 5)); System.out.println(lib.getMessage()); } }编译后生成头文件cd java_src javac NativeLib.java javac -h . NativeLib.java如果用的是JDK 8请用javac -h不是javac -jniJDK 8以前用的是javah命令JDK 8虽然还保留javah但建议直接用javac -h。成功后会生成一个NativeLib.h文件里面就是JNIEXPORT声明的函数原型。/* DO NOT EDIT THIS FILE - it is machine generated */ #include jni.h #ifndef _Included_NativeLib #define _Included_NativeLib #ifdef __cplusplus extern C { #endif JNIEXPORT jint JNICALL Java_NativeLib_add (JNIEnv *, jobject, jint, jint); JNIEXPORT jstring JNICALL Java_NativeLib_getMessage (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif请仔细看这个头文件所有函数都包裹在extern C里这就是为什么我们自己的.cpp实现文件也要包含这个头文件以确保函数定义时也是C链接。4.4 第三步CMakeLists.txt配置在项目根目录创建CMakeLists.txt重点是将jni.h所在的include目录和平台相关的目录暴露给编译器。cmake_minimum_required(VERSION 3.20) project(jni_native LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) # JDK 路径按你的实际JAVA_HOME修改 set(JAVA_HOME /usr/lib/jvm/java-11-openjdk-amd64) include_directories( ${JAVA_HOME}/include ${JAVA_HOME}/include/linux # macOS 下是 ${JAVA_HOME}/include/darwin ) add_library(native SHARED native_lib.cpp ) # macOS 需要额外设置安装名 if(APPLE) set_target_properties(native PROPERTIES INSTALL_RPATH loader_path BUILD_WITH_INSTALL_RPATH TRUE ) endif()注意Windows上的include目录是JAVA_HOME/include和JAVA_HOME/include/win32Linux是include/linuxmacOS是include/darwin。很多人在这一步漏了平台子目录导致找不到jni_md.h。4.5 第四步C侧实现实现文件里我们的函数签名要和生成的头文件完全一致。这里我用一个关键点示范jstring和jint混合处理。#include jni.h #include string #include NativeLib.h extern C { JNIEXPORT jint JNICALL Java_NativeLib_add(JNIEnv *env, jobject thiz, jint a, jint b) { return a b; } JNIEXPORT jstring JNICALL Java_NativeLib_getMessage(JNIEnv *env, jobject thiz) { std::string msg Hello from C JNI; return env-NewStringUTF(msg.c_str()); } }这个函数里出现了jstring和NewStringUTF这属于引用类型和JNI函数调用的范畴了但先记住一点jstring你不能当成普通字符串返回必须通过NewStringUTF或NewString创建。后面章节讲引用类型时我会重点展开。4.6 第五步编译并运行在CLion里直接点击Build生成libnative.so或libnative.dylib、native.dll。然后把生成的动态库放到Java代码能加载到的地方运行NativeLib。运行Java主类的方式可以直接用CLion的Run Configuration搭配IDEA也可以命令行java -Djava.library.path./build/lib NativeLibIdea自身也可以在VM options里配置-Djava.library.path指向你的动态库所在目录。第一次跑通这个流程你基本就对JNI的开发闭环有了整体感知Java定义native方法javac -h生成头文件C实现编译成一个动态库Java程序加载并调用。后面所有复杂功能包括字符串、数组、Java对象回调都是在这条流水线上扩展出来的。5. 基础数据类型实操案例从int到jcharArray的完整演示5.1 一个综合示例Java层传多种基础类型前面只演示了add函数这里我多给一个综合一点的例子涉及所有基础类型。Java端public class TypeDemo { static { System.loadLibrary(typedemo); } public native void passAllTypes(boolean boolVal, byte byteVal, char charVal, short shortVal, int intVal, long longVal, float floatVal, double doubleVal); public native long testLongReturn(); }生成头文件后C侧实现extern C JNIEXPORT void JNICALL Java_TypeDemo_passAllTypes(JNIEnv *env, jobject thiz, jboolean boolVal, jbyte byteVal, jchar charVal, jshort shortVal, jint intVal, jlong longVal, jfloat floatVal, jdouble doubleVal) { // 打印时全部显式转成可打印的类型 char cStr[64]; snprintf(cStr, sizeof(cStr), char code point: %u, static_castunsigned int(charVal)); printf(boolVal%d, byteVal%d, shortVal%d, intVal%d, longVal%lld\n, static_castint(boolVal), static_castint(byteVal), static_castint(shortVal), static_castint(intVal), static_castlong long(longVal)); printf(floatVal%.2f, doubleVal%.2f, %s\n, static_castdouble(floatVal), doubleVal, cStr); }这里的关键点在printf里。jboolean是unsigned char不能直接匹配%dfloat传给printf的%f之前必须转成double。如果你偷懒直接把jfloat当double传给printf在64位平台上可能会因为参数类型不匹配读到垃圾值。C语言的printf是类型不安全的这里我要再强调一次JNI函数内部不是避风港C/C的类型规则照常生效。5.2 jcharArray入参的处理示范传单个jchar很简单但是一旦遇到char数组事情就不一样了。看下面这个例子Java层传一个char[]过来。public native int calcCharArrayLength(char[] data);C侧实现extern C JNIEXPORT jint JNICALL Java_TypeDemo_calcCharArrayLength(JNIEnv *env, jobject thiz, jcharArray array) { if (array nullptr) { return 0; } jsize len env-GetArrayLength(array); jchar *elements env-GetCharArrayElements(array, nullptr); if (elements nullptr) { return 0; } // 这里临时用elements做一些只读操作 jsize count 0; for (jsize i 0; i len; i) { if (elements[i] ! 0) { count; } } env-ReleaseCharArrayElements(array, elements, JNI_ABORT); return count; }GetCharArrayElements拿回来的是底层缓冲区的指针这个指针可能指向Java堆的拷贝也可能直接指向原始数组取决于JVM实现和调用场景。用完之后必须Release否则会内存泄漏。第三个参数mode有三种取值0将修改拷贝回Java数组并释放原生数组相当于“写回”JNI_COMMIT将修改拷贝回Java数组但不释放原生数组JNI_ABORT不拷贝回Java数组直接释放原生数组小程序里很多人图省事永远传0但当你修改的是从Java传进来的只读数据时用JNI_ABORT性能更高避免一次无谓的拷贝回写。这是JNI性能优化里最基础的一课。5.3 jboolean的特异功能JNI_TRUE和JNI_FALSEJNI在jni.h里还定义了两个常量#define JNI_FALSE 0 #define JNI_TRUE 1实际写代码时请用这两个常量不要直接用数字0、1。同时因为jboolean是unsigned char不要把它和C的bool混用更不要拿它直接做某些系统API的bool参数。如果需要转换显示转换出来bool cppBool (jboolValue JNI_TRUE);这样避免了一个经典异常你把jboolean直接当作bool传给了某个C接口C接口内部检查value true或者is true时如果jboolean的值是1以外的其他非零值结果就是未定义行为。6. JNI中无处不在的JNIEnv指针理解它是理解一切的钥匙6.1 JNIEnv到底是什么JNIEnv是JNI Native Interface的核心句柄。每个native方法第一个参数都是它除非是JNI_CreateJavaVM那类特殊入口。它本质上是一个指向线程局部数据的指针这个数据里装着一整个函数表你调用的所有JNI函数NewStringUTF、GetArrayLength、CallIntMethod等都是这张表里的函数指针。可以把JNIEnv想象成一个“Java虚拟机在native层的翻译”。你通过它向JVM查询数组长度、创建Java对象、抛异常、访问字段。没有它JNI代码寸步难行。C和C访问JNIEnv的方式有很大差别C语言中JNIEnv是JNIEnv_结构体的指针调用时要写成(*env)-GetArrayLength(env, array)C中有内联函数包装可以直接写成env-GetArrayLength(array)两种调用方式等价但混用容易出错。我在C文件里写了(*env)-这种在C里写成env-这种都没问题。怕就怕C文件里混用了(*env)-虽然能编译但可读性很差。6.2 JNIEnv是与线程绑定的JNIEnv一个极度重要的特性是它绑定的是当前native调用所在的线程。你在一个线程里拿到JNIEnv如果把它存起来到了另一个线程再用轻则拿不到正确的引用对象重则导致进程崩溃。正确做法是在需要访问JNI的线程里通过JavaVM去获取对应的JNIEnv。在native方法里拿到JNIEnv时如果要跨线程使用可以先调用AttachCurrentThread把新线程挂载到JVM上再拿到这个线程的JNIEnv用完后再DetachCurrentThread。这里我提一个在Android或者服务端线程池场景中非常常见的坑。你有一个线程池里的工作线程想回调Java层方法直接用了之前主线程的JNIEnv结果“时好时坏”。本质上就是这个线程绑定问题。你需要存一个JavaVM全局引用而不是存JNIEnv局部引用。JavaVM *g_vm; // 在JNI_OnLoad或初始化时保存JavaVM JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { g_vm vm; return JNI_VERSION_1_8; } // 在子线程中获取JNIEnv JNIEnv *env nullptr; g_vm-AttachCurrentThread((void**)env, nullptr); // 使用env... g_vm-DetachCurrentThread();这个示例提前引入了JNI_OnLoad和JavaVM两个概念但其实是避不开的。基础数据类型那一层你可能暂时用不到但只要你继续深入JNI一定会撞上。6.3 JNI_OnLoadJNI环境的入口动态库被System.loadLibrary加载后JVM会先找JNI_OnLoad这个导出函数。如果找到了就调用它返回值必须是一个表示JNI版本号的常量比如JNI_VERSION_1_6、JNI_VERSION_1_8。这个函数的常见用途有三个保存全局的JavaVM指针注册native方法RegisterNatives做一些native侧的初始化工作对比起来不写JNI_OnLoad的库也能跑JVM会默认按JNI_VERSION_1_2规范去找native符号。但我建议所有正经的JNI库都写上因为后续全局引用管理、方法注册、日志初始化都需要它。7. 常见问题与排查技巧实录7.1 编译期报错找不到jni.h或jni_md.h这个问题90%的原因是CMAKE的include路径没配好。CLion里报错时仔细看是哪个头文件缺失jni.h缺失JAVA_HOME/include没加进来jni_md.h缺失JAVA_HOME/include/linux或win32或darwin没加进来。Windows用户经常会碰到另一个情况JAVA_HOME路径里带了空格比如C:\Program Files\Java\jdk-17CMake如果没处理好引号路径会被截断。解决办法是把JAVA_HOME路径用双引号包起来或者干脆设一个不含空格的JDK路径。7.2 运行时UnsatisfiedLinkError找不到符号UnsatisfiedLinkError可能的原因我按排名列出动态库没被系统找到java.library.path不对函数签名和Java native方法不对应类名/方法名/参数不一致C函数没有用extern CWindows上漏了JNIEXPORT动态库extension不对Linux一般是.somacOS是.dylib或.jnilibWindows是.dll检查顺序建议先确认库路径然后javap -s看Java侧的native签名再用nm命令查出动态库里的导出符号对不对比最后核对JNI函数签名。比如我经常用的排查命令# 查看动态库导出符号 nm -D libnative.so | grep Java_如果这里输出为空那就说明符号导出或者extern C有问题。7.3 native方法参数错位但没崩溃这类bug最磨人心态。表现为运行不报错但某个参数的数值总是错。我一般优先怀疑是类型宽度不匹配比如Java层是longC侧用了jint或者Java层是char16位C侧用了jchar16位结果又强转成了char8位。排查思路用javap -s反编译确认Java侧签名在native函数入口处把所有参数按二进制dump出来对比期望值检查所有用了printf的转换是否类型安全如果涉及数组检查GetXXXArrayElements后是否动过指针偏移。我还有一个习惯所有JNI函数入口第一行都用宏写一个“参数打印”日志把每个参数类型和值打印出来日志会极大缩短定位时间。实际项目里这个习惯救了我很多次。7.4 char相关的中文乱码问题Java的char和C/C的char不是一回事这在前面已经强调了。如果你在C层收到jchar数组并想转成UTF-8字符串输出千万别直接强转。推荐的做法是使用JNI的字符串翻译函数GetStringUTFChars把Java的String转成UTF-8格式的char*NewStringUTF把UTF-8的char*转成Java的String对于char[]数组想好你要的是Unicode码点还是UTF-8字节流后再动手。很多图像处理项目传byte[]就是这个原因字节流的语义明确不会和UTF-16的代理对打架。7.5 局部引用表溢出JNI里通过NewStringUTF、FindClass、NewObject等函数创建的都是局部引用local reference它们只在当前native方法调用期间有效且在一个线程中额度有限默认在某些JVM上是16KB或32KB以上具体因实现而异。基础数据类型阶段还不会频繁创建局部引用但你在循环里反复调用NewStringUTF时就会踩到这个坑。解法只有两种使用DeleteLocalRef显式释放或者让代码逻辑减少循环内引用创建。更深层的局部引用、全局引用管理会在后续章节单独细讲这里先埋个预告。8. 后续内容预告与个人经验JNI这套东西类型系统是第一个坎但不是最后一个坎。基础数据类型处理熟之后整个JNI的常用面就打开了。后续系列里我计划重点写这么几块第一部分是引用类型jstring、jobject、jclass这些东西的内部结构和访问方式特别是字符串处理的UTF-8和UTF-16各种坑第二部分是数组与直接缓冲区byte数组在图像、音视频、序列化场景里的高性能传输第三部分是字段与方法调用包括访问Java对象的实例字段、静态字段以及回调Java方法第四部分是全局引用和局部引用的管理策略这在长期运行的服务里直接决定内存稳定性第五部分是RegisterNatives手动注册方法很多工业级JNI库都用它绕开自动符号查找。当初我学JNI的时候啃完类型系统后最大的体会是所有难懂的概念最后都能落到内存布局和线程绑定这两个核心问题上。你理解了数据在Java堆和native堆之间怎么流动、谁手里握着的JNIEnv属于哪条线程后面再复杂的坑也都能推出来。这篇文章能写的内容还有很多但基础数据类型这层地基值得你花一个下午亲手敲一遍代码。我已经把CLion环境下从Java到C的闭环流程都列出来了照着走一遍再去解决那些报错和错位问题你对JNI的信心会完全不一样。
返回列表