c++oding="utf-8" ?>
java调用c++必须通过jni函数签名,因jvm与c++内存隔离,类型无法自动转换;签名错误会导致unsatisfiedlinkerror;需严格匹配包名、类名、方法名及jni类型编码。

Java 层无法直接调用 C++ 函数,必须通过 JNI 接口层中转;不写对 JNIEnv* 和线程绑定逻辑,调用会崩溃或返回乱码。
为什么 Java 调用 C++ 必须走 JNI 函数签名这道“关卡”
Java 虚拟机(ART)和 C++ 运行在完全隔离的内存空间里,java.lang.String 和 std::string 不能自动转换,int[] 和 int* 也不能直接传参。JNI 强制要求你显式声明类型映射规则——这就是函数签名(如 ([BI)I 表示输入 byte 数组和 int,返回 int)。漏写、写错签名,System.loadLibrary 不报错,但首次调用 native 方法时直接抛 UnsatisfiedLinkError。
常见错误现象:
- Java 层调用
NativeUtils.calcDesMac64()报No implementation found for native Lcom/mypackage/jni/CalcMac;.Native_JniCalcDesMac64 - C++ 函数名拼错,比如写成
Java_com_mypackage_jni_CalcMac_Native_JniCalcDesMac64却漏了下划线或大小写 - 没加
JNIEXPORT和JNICALL宏,导致符号未导出,链接失败
实操建议:
远程控制 Android 设备,支持截图、自动化操作手机 App 等功能。本应用使用 uiautomator2 作为底层框架,通过 ADB 连接设备,用户无需 Root(1)查看手机屏幕;(2)自动化操作手机 App;(3)远程安装/卸载应用……
- 用
javah -jni(旧版)或 Android Studio 自动生成头文件,别手敲函数名 - 签名必须严格匹配:包名用下划线替换点,类名和方法名之间用两个下划线,参数类型用 JNI 编码(
[B是 byte[],I是 int) - 函数声明必须带三个固定参数:
JNIEnv* env、jobject obj(或jclass cls对应 static 方法)、再跟 Java 参数
如何安全地在 C++ 中访问 Java 传入的 byte[] 并避免内存越界
Java 的 byte[] 在 JNI 中不是裸指针,而是 jbyteArray 类型,必须用 env->GetByteArrayElements() 或更推荐的 env->GetByteArrayRegion() 拷贝出来——前者可能触发 pinning(锁定内存),后者只读拷贝更轻量,也避免 ART GC 移动数组时出问题。
容易踩的坑:
- 直接强制转换
(jbyte*)array,运行时崩溃或读到脏数据 - 用了
GetByteArrayElements()却忘记调用ReleaseByteArrayElements(),造成内存泄漏 - 传入空数组或 null,没做
if (array == nullptr)判断,直接解引用
实操建议:
- 优先用
env->GetByteArrayRegion(array, 0, len, (jbyte*)buf)把数据拷进预分配的 C++ 缓冲区 - 如果必须原地操作(如加密 inplace),用
env->GetPrimitiveArrayCritical()+ReleasePrimitiveArrayCritical(),但要极短时间完成,否则阻塞 GC - 所有数组长度必须用
env->GetArrayLength(array)获取,不要信 Java 层传来的len参数(可能被篡改)
Java 层加载 so 库失败的三种典型原因和验证方式
System.loadLibrary("calcmac") 看似简单,实际背后涉及 ABI 匹配、路径查找、依赖解析三重校验。任一环节失败都静默失败,只在调用 native 方法时报 UnsatisfiedLinkError。
常见错误现象:
- App 在 arm64-v8a 设备上闪退,logcat 显示
dlopen failed: library "libcalcmac.so" not found - so 加载成功,但调用时报
java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "logd" - 模拟器上正常,真机上崩溃,因为 NDK 版本太新,targetSdkVersion 太低
实操建议:
- 确认 so 文件放在
src/main/jniLibs/arm64-v8a/libcalcmac.so(不是libs/),且 gradle 中没禁用对应 ABI:ndk { abiFilters 'arm64-v8a' } - 用
readelf -d libcalcmac.so | grep NEEDED检查是否依赖了高版本 libc(如 libc++.so.21),换成libc++_shared.so并打包进 apk - 在
static{}块里加 try-catch 包裹loadLibrary,并打印Log.e(TAG, "load failed", e),否则异常被吞掉
从 C++ 回调 Java 方法时 JNIEnv* 不能跨线程复用
Android 的 JNI 环境(JNIEnv*)是线程绑定的。C++ 在子线程(如 decode 线程、网络回调线程)里直接用主线程传来的 env 调用 env->CallVoidMethod(),大概率 crash 或静默失败。
根本原因:
-
JNIEnv*实际是线程局部存储(TLS)里的一个指针,每个线程有自己的副本 - 主线程的
env在子线程里是无效地址,ART 可能返回 null 或任意值
实操建议:
- 在 Java 层传入
jobject时,用env->NewGlobalRef(obj)创建全局引用,防止 Java 对象被 GC - 在子线程开头调用
g_jvm->AttachCurrentThread(&env, nullptr)获取当前线程的JNIEnv*(g_jvm是JavaVM*,在JNI_OnLoad里保存) - 退出前务必调用
g_jvm->DetachCurrentThread(),否则线程退出后 JVM 不释放资源 - 回调方法尽量用
env->CallStaticXXXMethod(),避免传jobject生命周期管理问题
最易被忽略的一点:JNIEnv* 不是“一次获取,到处可用”,它本质上是个临时句柄,生命周期严格绑定于当前线程上下文。任何跨线程传递或缓存 env 的做法,都是在埋雷。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










