【问题反馈】JNI 开发:为什么 C++ 在 Debug 正常,Release 却返回 NaN?
摘要:
在 Android NDK / JNI 开发中,经常会遇到这样一种“诡异”问题:Debug 模式下运行完全正常,而 Release 模式却出现 NaN、Infinity 甚至随机结果。
本文通过一次真实的 JNI 坐标转换案例,深入分析了该问题的根本原因——C++ 返回局部栈内存指针所导致的未定义行为(Undefined Behavior)。

【问题反馈】JNI 开发:为什么 C++ 在 Debug 正常,Release 却返回 NaN?
本文为以下问题的解决记录。由于问题较为典型,故梳理备忘。
https://github.com/eqgis/Sceneform-EQR/discussions/16

一、问题现象描述
1. 现象
- Debug 构建
- JNI 返回的坐标数值正常

- Release 构建
- 返回坐标中出现
NaN/Infinity
- 返回坐标中出现
且仅在Release出现

2. 出问题的方法
JNIEXPORT void JNICALL Java_com_eqgis_eqr_core_CoordinateUtilsNative_jni_1ToScenePosition( JNIEnv *env, jclass clazz, jdouble ref_x, jdouble ref_y, jdouble target_location_x, jdouble target_location_y, jdouble azimuth_rad, jdoubleArray outJNIArray){double*offset =ComputeTranslation(ref_x, ref_y, target_location_x, target_location_y);double deX =*offset;double deY =*(offset +1);double x = deX *cos(azimuth_rad)- deY *sin(azimuth_rad);double y = deX *sin(azimuth_rad)+ deY *cos(azimuth_rad);double outArray[]={x, y}; env->SetDoubleArrayRegion(outJNIArray,0,2, outArray);}二、问题根因定位:一个“看起来没问题”的函数
问题最终锁定在这个函数:
double*ComputeTranslation(double x1,double y1,double x2,double y2){double res[2]={0,0};... res[0]= flagX * x; res[1]= flagY * y;return res;}乍一看逻辑完全正确,但这里隐藏了一个致命错误。
三、致命问题:返回了栈内存指针(未定义行为)
1. res 是什么?
double res[2];res是 函数内部的局部变量- 存储在 当前函数的栈帧(stack frame)中
2. 函数返回后发生了什么?
当 ComputeTranslation 返回时:
- 函数栈帧被销毁
res对应的内存 立刻失效- 返回的指针指向:
- 已被释放的栈空间
- 或即将被复用的内存区域
3. C++ 标准如何定义这种行为?
这是典型的 Undefined Behavior(未定义行为)
含义是:
- 编译器不保证任何结果
- 程序:
- 可能“看起来能跑”
- 也可能随机崩溃
- 也可能只在 Release 模式出问题
四、为什么 Debug 正常,而 Release 出 NaN?
这是很多开发者最困惑的地方。
1. Debug 模式的特点
- 编译器优化极少
- 栈内存分配保守
- 局部变量:
- 生命周期“看起来”更长
- 内存内容不容易被覆盖
返回的指针虽然非法,但数据暂时还在
2. Release 模式的特点
- 启用
-O2 / -O3等激进优化 - 栈空间:
- 快速复用
- 指令重排
- 寄存器替代变量
编译器甚至可能认为:
“你返回这个指针是非法的,那我随便优化”
结果就是:
*offset变成随机值- 或直接成为
NaN / Inf
五、为什么 NaN 特别容易出现?
在 Release 下,offset 可能是:
- 未初始化内存
- 被 SIMD / 浮点寄存器覆盖
- 任意 bit pattern
而 浮点数中:
- 特定 bit pattern ⇒
NaN - 一旦参与计算:
NaN + x = NaNsin(NaN) = NaN
导致后续都是NaN
六、这不是 JNI 的问题
值得特别强调的是:
- JNI 只是一个函数调用边界
- 问题在 C++ 层就已经发生
- Java 侧只是“忠实地接收了 NaN”
如果这是纯 C++ 工程:
- 现象 完全一致
七、正确修复方式:返回值语义而不是指针
修复方案:使用结构体返回
structVec2{double x;double y;}; Vec2 ComputeTranslation(double x1,double y1,double x2,double y2){ Vec2 res{0.0,0.0};... res.x = flagX * x; res.y = flagY * y;return res;}调用:
Vec2 offset =ComputeTranslation(...);double deX = offset.x;double deY = offset.y;八、为什么“以前这么写也没事”?
原因通常是:
- 旧编译器优化弱
- Debug 构建长期被使用
- 数据规模较小
- 没踩到“恰好会覆盖栈”的场景
但:
未定义行为从来不是“偶尔才错”,而是“早晚会炸”。
九、如何系统性避免这类问题?
1. 永远不要返回局部变量地址
return&localVar;return localArray;2. 优先使用值语义
struct/ std::array / std::pair 3. Debug ≠ 正确
Debug 只能说明:
“在当前编译条件下恰好没炸”
十、总结
| 问题 | 结论 |
|---|---|
| Debug 正常 | 不代表代码正确 |
| Release 出 NaN | 典型 UB 表现 |
| 根因 | 返回栈内存指针 |
| JNI 是否有问题 | 没有 |
| 正确解法 | 返回结构体 / 值语义 |
这次问题再次验证了一点:
C++ 中,最危险的 Bug 往往不是“复杂算法”,
而是“看起来理所当然的代码”。
如果在 Debug / Release 行为不一致时遇到诡异问题,
第一时间检查:是否触发了未定义行为。