
1. 项目概述如果你正在开发一个需要极致性能、调用底层系统API或者复用大量C/C遗留代码的Java应用那么JNIJava Native Interface几乎是你绕不开的技术。它就像一座连接Java世界和C/C世界的桥梁让两者能够握手言和协同工作。但这座桥并不好走尤其是当你需要在桥的两端传递“字符串”这种复杂对象时稍有不慎就会遇到内存泄漏、程序崩溃或者乱码等棘手问题。我自己在早期做音视频编解码和游戏引擎集成时就曾在这上面栽过不少跟头一个字符串没处理好整个应用就莫名其妙地卡死或闪退。今天我们就来彻底拆解JNI中C与Java字符串互传这个核心且高频的“痛点”。这不仅仅是调用几个API那么简单它涉及到JVM内存管理、字符编码、引用生命周期等一系列底层概念。我将通过7个关键步骤从最基础的“Hello World”式传参到高性能、零拷贝的临界区操作再到如何安全地缓存方法ID以避免性能瓶颈手把手带你从零走到精通。无论你是刚接触JNI的开发者还是已经用过但总觉得心里不踏实的老手这篇文章都能帮你构建起清晰、稳固的知识体系让你在下次处理JNI字符串时能够胸有成竹游刃有余。2. 核心原理与前置知识理解JNI字符串的本质在动手写代码之前我们必须先搞清楚一个根本问题在JNI中Java的String和C的char*到底有什么不同为什么不能直接传递2.1 Java字符串与C字符串的内存模型差异Java中的String是一个不可变的对象它内部使用UTF-16编码在较新版本的JVM中对于纯Latin-1字符集的字符串可能会优化为byte数组但对外表现仍是UTF-16。这个对象存活在JVM管理的堆内存中由垃圾回收器GC负责生命周期管理。当你把一个Java String对象作为参数传给native方法时你传递的实际上是一个jstring类型的引用它指向JVM内部的那个String对象。而C语言中的字符串通常就是一个以\0空字符结尾的字符数组char*它使用的是如ASCII、UTF-8或本地代码页等编码存放在native堆或栈上其内存需要开发者手动管理分配和释放。这两种模型天差地别。你不能直接把jstring当作char*来用因为jstring只是一个“句柄”或“引用”它背后指向的是一块JVM内部格式的内存。直接操作这个引用是无效且危险的。2.2 JNIEnv你的操作手册和通行证JNIEnv*JNI Environment Pointer是每个JNI本地函数第一个参数对于实例方法第二个参数是jobject或jclass。你可以把它理解为一个包含了所有JNI函数指针的操作手册同时也是你在JVM中执行操作的通行证。所有对Java对象的操作包括获取字符串内容、创建新字符串、调用Java方法等都必须通过JNIEnv提供的函数来完成。对于C和C使用方式略有不同C语言JNIEnv是一个指向函数表指针的指针所以调用函数时需要解引用形如(*env)-GetStringUTFChars(env, ...)。C语言JNIEnv是一个封装好的类调用函数更简洁形如env-GetStringUTFChars(...)。本文将以C语言为例进行讲解但原理完全相通。理解JNIEnv是理解后续所有API调用的基础。2.3 字符编码UTF-8与UTF-16的转换战场编码问题是字符串互传中乱码的罪魁祸首。JNI主要涉及两种编码转换UTF-8一种变长编码兼容ASCII是C/C世界和网络传输中非常常见的格式。JNI函数名中的“UTF”指的就是修改过的UTF-8Modified UTF-8它与标准UTF-8在处理null字符和补充字符上略有不同但对于绝大多数常用字符两者是一致的。UTF-16Java字符串在内存中的原生格式。每个字符通常占用2个字节对于基本多文种平面BMP的字符。JNI提供两组主要的字符串操作函数一组用于操作UTF-8编码的字符串如GetStringUTFChars另一组用于操作UTF-16编码的Unicode字符串如GetStringChars。在跨平台应用中特别是涉及中文等非ASCII字符时选择哪一组函数需要谨慎我们会在后续步骤中详细讨论。3. 关键步骤一从Java到C——获取并释放字符串内容这是最基础也是最常用的一步在C代码中读取Java传过来的字符串。3.1 使用GetStringUTFChars/ReleaseStringUTFChars这是最经典、最安全的“拷贝”方式。它的工作流程是JNI函数向JVM申请将Java String对象内部的UTF-16数据转换为UTF-8格式并可能将转换后的字节数组拷贝到一个新分配的C风格内存空间中然后返回指向这块内存的指针。#include jni.h #include stdio.h JNIEXPORT void JNICALL Java_Example_printString (JNIEnv *env, jobject obj, jstring javaStr) { const char *c_str NULL; jboolean isCopy; // 步骤1: 获取UTF-8格式的C字符串指针 c_str (*env)-GetStringUTFChars(env, javaStr, isCopy); if (c_str NULL) { // 获取失败通常是因为内存不足应直接返回避免后续操作 return; } // 步骤2: 使用C字符串此时是安全的 printf(C received: %s\n, c_str); printf(Is this a copy? %s\n, (isCopy JNI_TRUE) ? Yes : No); // 步骤3: 至关重要释放由GetStringUTFChars获取的资源 (*env)-ReleaseStringUTFChars(env, javaStr, c_str); }关键点解析isCopy参数这是一个输出参数。如果JVM选择将字符串内容拷贝到新的内存返回JNI_TRUE那么c_str指向的就是一块全新的、由JNI分配的内存。如果JVM可以直接返回一个指向Java字符串内部数据的指针返回JNI_FALSE则没有额外拷贝。你不能依赖isCopy的值来做逻辑判断因为它是JVM的实现细节不同版本、不同情况可能不同。你只需要记住无论是否拷贝都必须配对调用ReleaseStringUTFChars。NULL检查GetStringUTFChars可能因为内存不足等原因失败返回NULL。良好的习惯是总是检查返回值。释放操作ReleaseStringUTFChars是必须的。如果之前是拷贝它会释放那块拷贝出来的内存如果不是拷贝它则执行一些必要的内部清理。如果不释放会导致内存泄漏。实操心得我习惯把Get和Release的调用像malloc/free一样成对看待并且在获取后立即进行NULL检查这能避免很多潜在的崩溃问题。3.2 使用GetStringCritical/ReleaseStringCritical高性能但限制多当你需要极致的性能并且要在C代码中长时间、大量地读取Java字符串内容时可以考虑这对函数。它们的目标是尽可能地返回一个指向Java字符串原始内存UTF-16格式的直接指针从而避免拷贝。JNIEXPORT void JNICALL Java_Example_criticalString (JNIEnv *env, jobject obj, jstring javaStr) { const jchar *c_str_unicode NULL; // 步骤1: 进入临界区获取直接指针 c_str_unicode (*env)-GetStringCritical(env, javaStr, NULL); if (c_str_unicode NULL) { return; } // 步骤2: 在临界区内进行操作 // 注意c_str_unicode指向的是UTF-16编码的jchar数组 // 你可以直接读取例如计算长度或进行某些不需要解码为UTF-8的二进制操作 jsize len (*env)-GetStringLength(env, javaStr); for (int i 0; i len; i) { printf(%04x , c_str_unicode[i]); // 打印UTF-16代码单元 } printf(\n); // 步骤3: 离开临界区释放“锁” (*env)-ReleaseStringCritical(env, javaStr, c_str_unicode); }严苛的限制与风险不能阻塞在GetStringCritical和ReleaseStringCritical之间绝对不能调用任何可能阻塞当前线程的JNI函数或其他函数。这包括创建新的Java对象、进行复杂的I/O操作等。不能调用大部分JNI函数在这对函数之间你只能调用极少数“无害”的JNI函数如GetStringLength。像FindClass,NewObject等可能触发GC或分配内存的函数是禁止的。目的这对函数会暂停垃圾回收器GC。如果在这期间GC被其他线程触发整个系统可能会被阻塞导致性能下降甚至死锁。注意事项除非你进行的是非常底层、对性能极度敏感且操作时间极短的字符串处理比如内存拷贝、哈希计算否则建议优先使用GetStringUTFChars。滥用GetStringCritical是导致JNI代码不稳定和难以调试的常见原因。4. 关键步骤二从C到Java——创建新的Java字符串处理完数据后我们常常需要将结果从C传回Java。这时就需要在C端创建一个新的Java String对象。4.1 使用NewStringUTF这是最常用的方法用于从一个UTF-8编码的C字符串char*创建一个新的Javajstring对象。JNIEXPORT jstring JNICALL Java_Example_createString (JNIEnv *env, jobject obj) { const char *c_str Hello from C! 你好C语言; // 步骤直接创建jstring jstring java_str (*env)-NewStringUTF(env, c_str); // 注意这里不需要检查java_str是否为NULL吗需要 if (java_str NULL) { // 创建失败可能因为传入的C字符串不是合法的UTF-8序列或者内存不足 // 在C中我们可以返回NULLJava层会收到一个null return NULL; } return java_str; }关键点解析编码要求传入的C字符串必须是有效的修改版UTF-8Modified UTF-8编码。对于标准ASCII字符和中文字符使用3字节UTF-8这通常没问题。但如果你的C字符串包含嵌入的\0字符标准UTF-8允许或者来自某些Windows API的特定编码字符串可能会出现问题。内存管理创建的jstring对象是一个局部引用Local Reference。当你的本地方法返回它给Java层后Java层会接管它的生命周期。如果本地方法没有返回它或者你在本地方法内部创建了但不返回则需要考虑在适当的时候调用DeleteLocalRef来释放局部引用防止局部引用表溢出后面会详细讲。4.2 使用NewString操作UTF-16如果你在C端已经拥有了UTF-16编码的jchar数组或者你需要精确控制每个Unicode代码点可以使用NewString。JNIEXPORT jstring JNICALL Java_Example_createStringUnicode (JNIEnv *env, jobject obj) { // 一个包含“Hello”和汉字“世界”的UTF-16数组 (Big-Endian) jchar unicode_chars[] {0x0048, 0x0065, 0x006c, 0x006c, 0x006f, 0x4e16, 0x754c}; jsize len 7; // 数组长度 jstring java_str (*env)-NewString(env, unicode_chars, len); if (java_str NULL) { return NULL; } return java_str; }这种方式更底层通常在你需要直接处理Unicode代码单元时使用。5. 关键步骤三安全地处理字符串区域避免拷贝有时候我们只想读取Java字符串的一部分或者将一个C字符串的内容直接设置到Java字符串的某个位置而不想进行完整的拷贝。这时可以使用GetStringRegion和SetStringRegion系列函数。5.1 GetStringUTFRegion读取部分内容到预分配缓冲区这个函数将Java字符串指定范围的字符转换为UTF-8复制到你提供的C缓冲区中。它不会分配新内存因此也不需要配对的释放函数。JNIEXPORT void JNICALL Java_Example_getSubString (JNIEnv *env, jobject obj, jstring javaStr) { char buffer[128]; // 预分配的栈上缓冲区 jsize start 6; // 从第6个字符开始Java String的char偏移 jsize len 5; // 获取5个字符 // 清空缓冲区是个好习惯 memset(buffer, 0, sizeof(buffer)); // 将javaStr中从start开始的len个字符转换为UTF-8复制到buffer中 (*env)-GetStringUTFRegion(env, javaStr, start, len, buffer); // 现在buffer中包含了子字符串的UTF-8内容 printf(Substring in C: %s\n, buffer); // 注意buffer必须足够大能够容纳转换后的字节包括结尾的\0。 // GetStringUTFRegion会确保不超过缓冲区大小但如果缓冲区太小会抛出StringIndexOutOfBoundsException。 }优点无内存管理负担因为使用栈或预先分配好的堆缓冲区无需调用Release。线程安全不涉及获取直接指针和暂停GC更安全。适合小数据量对于获取字符串的一部分内容非常高效。5.2 SetStringUTFRegion修改Java字符串的部分内容与Get对应SetStringUTFRegion允许你用C字符串UTF-8的内容去替换Java字符串中指定范围的字符。JNIEXPORT void JNICALL Java_Example_modifyString (JNIEnv *env, jobject obj, jstring javaStr) { // 注意javaStr在Java层是final的此操作实际上会修改传入的jstring所引用的内容吗 // 不Java String是不可变的。这个函数会修改jstring这个引用所指向的底层数据吗 // 实际上SetStringUTFRegion通常用于操作通过NewStringUTF等创建的、尚未“固化”的字符串 // 或者用于操作字符数组jcharArray。对于从Java层传入的String对象直接修改其内容的行为是未定义的并且可能破坏JVM的字符串常量池等机制极其危险 // 因此SetStringUTFRegion的常见用法是配合NewStringUTF先创建一个空或占位字符串然后再填充内容但这并不常见。 // 更安全的做法是在C端构造好新的C字符串然后通过NewStringUTF返回一个全新的jstring给Java。 // 以下为**危险示例**仅用于理解API实际开发中应避免 // jstring mutableStr (*env)-NewStringUTF(env, Initial Value); // const char* replacement NewPart; // (*env)-SetStringUTFRegion(env, mutableStr, 0, 7, replacement); // 替换前7个字符 // return mutableStr; // 返回修改后的新字符串 }重要警告SetStringUTFRegion不能用于修改从Java层传入的String对象的内容因为JavaString是不可变的。尝试这样做会导致未定义行为很可能使JVM崩溃。它的正确用途非常有限通常是在创建新字符串的复杂过程中进行填充。对于绝大多数场景“修改”字符串的正确模式是在C端创建新的内容然后通过NewStringUTF返回一个全新的jstring。6. 关键步骤四管理JNI引用防止内存泄漏JNI引用管理是进阶必备知识处理不好轻则内存泄漏重则程序崩溃局部引用表溢出。字符串作为对象自然也遵循引用规则。6.1 局部引用Local Reference及其自动管理大多数JNI函数返回的引用如FindClass,NewStringUTF,GetObjectField返回的jstring都是局部引用。它们在本机方法执行期间有效并在方法返回时由JVM自动释放。但是在以下情况你需要手动干预创建大量局部引用例如在一个循环中不断创建字符串。长时间运行的本机方法比如一个执行复杂计算或等待事件的native方法。JNIEXPORT void JNICALL Java_Example_manyLocalRefs (JNIEnv *env, jobject obj) { for (int i 0; i 1000; i) { jstring localStr (*env)-NewStringUTF(env, A temporary string); // 使用localStr... // 如果不手动删除在循环中快速创建1000个局部引用很容易超出局部引用表的默认容量通常512个。 (*env)-DeleteLocalRef(env, localStr); // 及时清理 } }6.2 使用PushLocalFrame/PopLocalFrame进行批量管理这是管理局部引用的最佳实践尤其适用于创建多个局部引用的复杂函数。它像是一个“作用域”或“栈帧”在这个帧内创建的所有局部引用在帧弹出时会被批量自动释放。JNIEXPORT jobject JNICALL Java_Example_complexOperation (JNIEnv *env, jobject obj, jstring input) { jobject result NULL; jstring tempStr1 NULL; jstring tempStr2 NULL; // 步骤1: 压入一个新的局部引用帧可以指定该帧能容纳的引用最小数量 if ((*env)-PushLocalFrame(env, 10) 0) { // 压入失败内存不足 return NULL; } // 步骤2: 在这个帧内放心创建局部引用无需单独Delete tempStr1 (*env)-NewStringUTF(env, Intermediate 1); if (tempStr1 NULL) goto pop_and_exit; // 错误处理跳转到清理 tempStr2 (*env)-NewStringUTF(env, Intermediate 2); if (tempStr2 NULL) goto pop_and_exit; // ... 复杂的业务逻辑可能创建更多临时引用 ... // 步骤3: 创建我们需要返回的结果对象 result (*env)-NewStringUTF(env, Final Result); if (result NULL) goto pop_and_exit; // 步骤4: 弹出局部引用帧释放tempStr1, tempStr2等所有在此帧内创建的局部引用 // 但result会被“提升”到前一个帧所以它不会被释放可以安全返回。 result (*env)-PopLocalFrame(env, result); return result; pop_and_exit: // 发生错误时弹出帧并丢弃所有临时引用返回NULL (*env)-PopLocalFrame(env, NULL); return NULL; }优点代码更简洁无需在每一个错误返回点都写一堆DeleteLocalRef大大减少了内存泄漏的风险。6.3 全局引用Global Reference与弱全局引用Weak Global Reference全局引用通过NewGlobalRef创建生命周期跨越多个本地方法调用甚至多个线程。必须用DeleteGlobalRef显式释放。常用于缓存jclass或jmethodID但通常缓存ID本身即可无需缓存jclass的全局引用因为jclass本身是局部引用缓存它需要升级为全局引用且可能阻碍类卸载。弱全局引用通过NewWeakGlobalRef创建它不会阻止垃圾回收器回收其引用的对象。在使用前必须用IsSameObject与NULL比较检查对象是否已被回收。对于字符串除非你有极其特殊的跨线程、长生命周期共享需求否则几乎不需要将其创建为全局或弱全局引用。字符串通常作为参数传入或结果返回其生命周期由Java层控制。7. 关键步骤五处理中文字符与编码陷阱中文字符是检验JNI字符串处理是否正确的“试金石”。乱码问题几乎都源于编码转换错误。7.1 确保源字符串编码正确问题往往始于Java层。确保你的Java源代码文件本身的编码如UTF-8与编译环境匹配。在IDE中检查项目编码设置。// Java端字符串常量“你好”在源码中的字节表示必须正确 public native void processChinese(String str); public static void main(String[] args) { // 明确使用Unicode转义序列可以避免源码编码问题但不直观 // String s1 \u4f60\u597d; // “你好”的Unicode转义 String s2 你好; // 确保你的.java文件是UTF-8编码且编译器以UTF-8读取它 new MyClass().processChinese(s2); }7.2 在C层使用正确的JNI函数对于包含中文的字符串GetStringUTFChars和NewStringUTF在大多数情况下是可行的因为它们处理的是修改版UTF-8而UTF-8能够表示所有Unicode字符。JNIEXPORT void JNICALL Java_MyClass_processChinese (JNIEnv *env, jobject obj, jstring javaStr) { const char* utf8 (*env)-GetStringUTFChars(env, javaStr, NULL); if (utf8) { // 在Linux/macOS终端如果终端支持UTF-8打印中文没问题 printf(C层收到(UTF-8): %s\n, utf8); // 计算的是UTF-8的字节数不是字符数 printf(字节数: %zu\n, strlen(utf8)); // 获取Java字符串的字符数UTF-16代码单元数 jsize len (*env)-GetStringLength(env, javaStr); printf(Java字符串长度(代码单元): %d\n, len); (*env)-ReleaseStringUTFChars(env, javaStr, utf8); } }潜在问题如果你的C代码需要将字符串传递给另一个只接受特定本地编码如Windows下的GBK的库那么你需要进行二次转换。这时可以考虑使用GetStringChars获取UTF-16的jchar*然后使用像iconv或WideCharToMultiByte这样的库函数进行转换。7.3 使用GetStringChars处理UTF-16如果你需要在C端直接操作Unicode代码点或者进行不涉及编码转换的二进制处理可以使用GetStringChars。JNIEXPORT void JNICALL Java_MyClass_handleUnicode (JNIEnv *env, jobject obj, jstring javaStr) { const jchar* unicode_str NULL; jboolean isCopy; unicode_str (*env)-GetStringChars(env, javaStr, isCopy); if (unicode_str) { jsize len (*env)-GetStringLength(env, javaStr); for (int i 0; i len; i) { // 直接访问UTF-16代码单元 printf(U%04x , (unsigned short)unicode_str[i]); } printf(\n); (*env)-ReleaseStringChars(env, javaStr, unicode_str); } }8. 关键步骤六性能优化与缓存策略在频繁进行JNI调用的场景下字符串操作的性能开销不容忽视。除了选择正确的API缓存是关键。8.1 缓存jmethodID和jfieldID查找方法ID或字段IDGetMethodID,GetStaticFieldID等是一个相对昂贵的操作因为它涉及字符串查找和反射。这些ID在同一个JVM进程内对于同一个类来说是稳定的因此应该被缓存起来。// 全局变量缓存ID注意线程安全如果多线程使用需要考虑初始化时机 static jmethodID g_mid_toString NULL; JNIEXPORT void JNICALL Java_MyClass_nativeMethod (JNIEnv *env, jobject obj) { jclass clazz; jstring resultStr; // 第一次调用时初始化缓存 if (g_mid_toString NULL) { clazz (*env)-GetObjectClass(env, obj); // 查找java.lang.Object的toString方法 g_mid_toString (*env)-GetMethodID(env, clazz, toString, ()Ljava/lang/String;); (*env)-DeleteLocalRef(env, clazz); // 删除临时的局部引用 if (g_mid_toString NULL) { return; // 方法查找失败 } } // 后续调用直接使用缓存的ID resultStr (*env)-CallObjectMethod(env, obj, g_mid_toString); // ... 处理resultStr ... }缓存位置本地静态变量如上例简单但需注意多线程竞争。可以在类加载时JNI_OnLoad或第一次调用时惰性初始化。Java静态初始化器更优雅的方式是在Java类的静态块中调用一个native方法进行初始化将ID缓存在Java侧的静态变量中通过SetStaticIntField等但这需要更多JNI交互。注意事项jclass类引用本身是局部引用不能直接缓存为全局静态变量。如果你想缓存类引用必须使用NewGlobalRef将其升级为全局引用并在适当的时候如JNI_OnUnload释放。但通常缓存jmethodID/jfieldID就够了它们不是引用只是整数ID。8.2 避免不必要的字符串转换和拷贝使用GetStringRegion代替GetStringUTFChars如果你只需要读取字符串的一部分或者字符串不大使用预分配缓冲区的GetStringRegion可以避免一次潜在的内存分配和拷贝。评估isCopy虽然不能依赖其值但在调试阶段观察isCopy可以帮助你了解JVM的行为。如果频繁返回JNI_TRUE拷贝且字符串很大可能是性能瓶颈。减少JNI调用次数最大的开销往往在JNI调用本身。设计接口时尽量一次传递足够的数据比如使用数组或结构化对象而不是多次调用传递单个字符串。9. 关键步骤七异常处理与错误检查JNI函数调用可能会抛出Java异常但C代码不会自动中断。异常会挂起在JVM中你必须主动检查并处理。9.1 检查异常在调用可能抛出异常的JNI函数后特别是调用Java方法Call*Method、创建对象NewObject、查找不存在的类或方法FindClass/GetMethodID必须检查是否有异常发生。JNIEXPORT void JNICALL Java_Example_exceptionDemo (JNIEnv *env, jobject obj, jstring javaStr) { const char* c_str (*env)-GetStringUTFChars(env, javaStr, NULL); // GetStringUTFChars 一般不会抛异常但可能返回NULL jclass clazz (*env)-FindClass(env, com/example/NonExistentClass); // FindClass 如果类找不到会抛出 NoClassDefFoundError if (clazz NULL) { // 检查异常是否发生 jthrowable exc (*env)-ExceptionOccurred(env); if (exc) { // 1. 打印异常栈信息调试用 (*env)-ExceptionDescribe(env); // 2. 清除异常防止影响后续JNI调用或导致Java层崩溃 (*env)-ExceptionClear(env); // 3. 进行错误处理例如返回默认值或设置错误码 printf(Class not found, clearing exception.\n); } // 即使清除了异常FindClass也失败了clazz为NULL应安全返回 if (c_str) (*env)-ReleaseStringUTFChars(env, javaStr, c_str); return; } // 调用一个可能抛出异常的方法 jmethodID mid (*env)-GetMethodID(env, clazz, someMethod, ()V); if (mid ! NULL) { (*env)-CallVoidMethod(env, obj, mid); // 检查调用后是否有异常 if ((*env)-ExceptionCheck(env)) { // ExceptionCheck比ExceptionOccurred更轻量只检查不获取引用 printf(Java method threw an exception!\n); // 通常在native方法中抛出异常后应该立即返回让异常传播到Java层。 // 或者清理资源后抛出一个新的异常。 (*env)-ExceptionClear(env); // 如果我们决定自己处理而不是传播 // 或者抛出一个新的C层定义的异常 jclass newExcCls (*env)-FindClass(env, java/lang/IllegalStateException); if (newExcCls ! NULL) { (*env)-ThrowNew(env, newExcCls, Native operation failed after Java exception); } } } if (c_str) (*env)-ReleaseStringUTFChars(env, javaStr, c_str); if (clazz) (*env)-DeleteLocalRef(env, clazz); }9.2 资源清理与异常安全确保在发生异常或函数提前返回时已获取的资源如字符串指针、局部引用能被正确释放。这通常意味着使用goto跳转到统一的清理标签或者使用PushLocalFrame/PopLocalFrame。JNIEXPORT jstring JNICALL Java_Example_safeOperation (JNIEnv *env, jobject obj, jstring input) { const char* c_input NULL; jstring result NULL; char* buffer NULL; c_input (*env)-GetStringUTFChars(env, input, NULL); if (c_input NULL) goto cleanup; // 获取失败跳转清理 buffer (char*)malloc(1024); if (buffer NULL) { // 内存分配失败可以抛出OutOfMemoryError jclass excCls (*env)-FindClass(env, java/lang/OutOfMemoryError); if (excCls ! NULL) { (*env)-ThrowNew(env, excCls, Failed to allocate memory in native code); } goto cleanup; } // ... 一些可能失败的操作 ... if (some_operation_failed) { // 操作失败抛出自定义异常 jclass excCls (*env)-FindClass(env, com/example/NativeException); if (excCls ! NULL) { (*env)-ThrowNew(env, excCls, Native operation X failed); } goto cleanup; // 跳转到清理释放资源然后让异常传播 } // 一切正常创建结果字符串 result (*env)-NewStringUTF(env, Success); cleanup: // 统一的资源释放块 if (c_input) { (*env)-ReleaseStringUTFChars(env, input, c_input); } if (buffer) { free(buffer); } // 注意如果上面抛出了异常result可能为NULL这没关系返回NULL即可。 return result; }遵循这七个关键步骤你就能系统地掌握JNI中字符串互传的核心技术、规避常见陷阱并写出高效、稳定的本地代码。记住JNI编程是精细活对细节的把握决定了代码的质量。多写、多测、多思考内存和异常是通往精通的必经之路。