ARTICLE DETAIL

资讯详情

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

Java调用DLL实战:JNI/JNA、IDEA配置与报错排查

Java调用DLL实战:JNI/JNA、IDEA配置与报错排查 做 Java 后端的人迟早会碰到一个场景手上有一份厂商给的 DLL只有 C 头文件和一份 PDF 说明让你在 IDEA 里用 Java 调起来。第一次遇到时我以为是加个依赖的事结果折腾了整整两天——报错从UnsatisfiedLinkError: no xxx in java.library.path一路换到找不到指定的模块再换到动态链接库(DLL)初始化例程失败每一个报错指向的原因都不一样。后来把这类需求反复做了十几遍才慢慢总结出一套相对固定的处理顺序先确认位数再确认依赖再确认加载路径最后才去看接口映射对不对。这篇内容就是把这套顺序完整摊开讲。它适合三类人一是第一次接触 JNI/JNA、需要在 IDEA 里调本地库的 Java 开发者二是手里有一堆老旧 C/C 库、被要求用 Java 包一层的工程同学三是已经能跑通但在打包发布、长期运行阶段不断踩坑的人。我会讲清楚 Java 调用 DLL 这件事的三条技术路线怎么选、IDEA 里那几处配置为什么必须那样设、报错栈怎么反推出真实原因以及从一个能跑的 Demo 到别人机器上也能跑中间还差哪几步。1. Java 跨到原生层的三条路以及选错路的代价1.1 什么情况下真的绕不开 DLLJava 本身没有直接调用 C ABI 的能力这一点在语言层面就定死了Java 的方法调用走的是字节码 虚表那一套函数签名里没有调用约定导出序号结构体对齐这些概念。所以只要目标功能只以 DLL 形式提供就必然有一层桥。常见到绕不开的场景大概有这几类。硬件 SDK身份证阅读器、指纹仪、读卡器、加密狗、某些工业相机厂商的交付物基本是一个 DLL 加一份头文件。工控与产线PLC、CAN 总线工具比如做诊断时需要的 SeedKey 算法库、扫码枪很多都是原生库。算法库复用已经有成熟的 C 实现比如 CMAC(AES128) 这类算法、图像处理、信号处理重写一遍成本太高。还有一类比较隐晦的Java 侧要用某个打印控件或者 Office 文档处理组件官方只给了 COM/DLL 接口。判断要不要走原生调用我一般用一条线如果这个能力用纯 Java 生态里的库能实现且性能可接受就别碰 DLL。因为一旦跨过去你就同时承担了两套内存模型、两套异常体系、两套部署依赖出问题的排查成本是纯 Java 的好几倍。1.2 手写 JNI、JNA 映射、进程外调用各自适合什么三条路线我都用过结论是能进程外就别进程内能 JNA 就别手写 JNI。维度手写 JNIJNA进程外调用上手成本高要写 C 胶水层、要编译链接低只写 Java 接口中要写一个本地可执行程序或本地服务运行时开销最低接近原生每次调用有映射开销单次约微秒级进程间通信开销毫秒级类型安全编译期发现不了全靠运行部分运行期校验由你自己定义的协议决定崩溃影响DLL 崩了 JVM 一起崩同左只崩本地进程Java 侧可重试部署复杂度高DLL 与 JVM 位数必须严格一致同左低两侧解耦位数可以不一致适合场景高频调用、回调复杂、需要精确控制引用接口不多、调用不密集、想快速验证DLL 版本混乱、32/64 位冲突、DLL 本身不够可靠手写 JNI 的代价不在写而在维护DLL 升级一次头文件可能变你的胶水层要跟着重编DLL 的位数变了你得准备两套编译产物。JNA 的好处是它用 Java 接口描述符号运行时自己去GetProcAddress中间没有编译环节改个方法名就能试。缺点是它对 C 的复杂类型联合体、位域、函数指针数组支持有限而且出问题时栈里全是 JNA 的帧定位会绕一点。进程外这条路经常被忽略但它是处理祖传 DLL最省心的方案。我遇到过一个只提供 32 位版本的老库而我们线上是 64 位 JVM改写又不可能。最后做法是把 DLL 包进一个小小的本地可执行程序里通过标准输入输出传 JSONJava 侧用ProcessBuilder起进程。性能确实差了两个数量级但那个功能一天调用不到一千次完全够用而且再也不用担心 JVM 被带崩。2. IDEA 里那几处不显眼、但决定成败的环境设置2.1 第一位永远是位数对齐UnsatisfiedLinkError: Cant load IA 32-bit .dll on a AMD 64-bit platform这条报错几乎每个第一次做这件事的人都见过。它的意思很直白你加载的 DLL 是 32 位的而 JVM 是 64 位的。反过来也成立。怎么确认先看 JVMSystem.out.println(System.getProperty(sun.arch.data.model)); // 32 或 64 System.out.println(System.getProperty(java.home));再看 DLL 的 PE 头。装了 Visual Studio 的话dumpbin /headers demo.dll | findstr machine输出里8664表示 x6414C表示 x86。没有 VS 的话用任意 PE 查看工具看机器类型字段也一样。这里有个 IDEA 特有的坑Project SDK、Gradle/Maven 用的 JDK、Run Configuration 实际启动的 JVM可能是三个不同的东西。项目结构里显示的是 64 位 JDK但 Gradle JVM 设成了 32 位跑gradle bootRun起来的就是 32 位进程。排查时不要相信界面在main里打一行上面那段代码看真实输出。我在这个问题上浪费过半天最后发现是 Gradle 的 JVM 下拉框选错了。2.2 DLL 搜索路径的三种挂法选错了打包就会崩System.loadLibrary(demo)最终会去java.library.path里找demo.dll。这个属性在 Windows 上是从PATH派生的也就是说你改了系统环境变量必须重启 IDEA 才生效——因为 JVM 启动时就把这个属性算好了运行期再改System.setProperty没有用这也是很多人明明改对了却还是不生效的原因。IDEA 里挂载 DLL 通常有三种方式各有适用面方式做法优点缺点VM options 指定路径Run Configuration 里加-Djava.library.pathD:\libs改起来最快调试方便只在本机生效无法打包分发丢到工作目录把 DLL 放到 Run Configuration 的 Working directory 下不用改配置依赖当前目录这个隐式约定换环境就废资源目录 运行期解压DLL 放src/main/resources/native/启动时复制到临时目录再System.load能跟着 jar 一起发布多写十几行代码调试阶段用第一种最省事但只要这个项目要交付给别人就必须换成第三种。第二种我建议直接放弃因为它让程序能不能跑依赖于你从哪个目录启动它早晚出事。VM options 的具体位置在 Run/Debug Configurations 里注意要填在VM options那一栏而不是Program arguments。有一个细节java.library.path里的多个路径在 Windows 上用分号分隔在 Linux/macOS 上用冒号。跨平台项目里硬编码这个属性很容易翻车不如走资源解压路线。2.3 依赖 DLL 才是真正的隐形杀手这是我想重点讲的一点报错说找不到指定模块的时候十有八九目标 DLL 就在那儿缺的是它自己依赖的东西。Windows 的LoadLibrary是递归加载的。你加载demo.dll它内部静态链接了vcruntime140.dll、msvcp140.dll或者厂商另外给的third_party.dll任何一个找不到LoadLibrary都会失败而错误信息只会告诉你找不到指定的模块不会告诉你是哪一个。排查方法dumpbin /dependents demo.dll这会把直接依赖列出来。如果依赖层级深用 Dependencies开源的那个可视化工具或 Process Monitor 更直观——Proc Monitor 挂上Process Monitor的过滤器只留Load Image和CreateFile事件跑一次 Java 程序看它到底去哪些目录找了哪些 DLL结果一目了然。这个方法笨但绝对有效我遇到的所有诡异加载失败最后都是靠它定位的。还有一个容易忽略的点VC 运行库的版本。目标机器上装了 VS 2015-2022 的合并运行库通常就够了但如果 DLL 是更老的编译器产物可能依赖msvcr120.dll这类老版本。这种情况别去网上随手下载 DLL 文件往System32里塞——这是最危险的做法来源不明的 DLL 可能被替换过也可能覆盖掉系统正在用的同名文件导致别的软件起不来。正确做法是装对应的官方运行库安装包或者请厂商重新用较新的工具链编译一份。3. 用 JNA 跑通第一个 DLL从 C 源码到 Java 调用3.1 先编一个最小的 DLL把变量控制住排错最忌讳在真实业务库上做实验。先自己写一个二十行的 DLL把能不能加载能不能调用参数怎么传这三件事验证孤立地跑通再去接真实库。// demo.c #include stdint.h #include string.h #ifdef _WIN32 #define API __declspec(dllexport) #else #define API #endif typedef struct { int32_t id; char name[32]; double score; } Student; API int32_t add(int32_t a, int32_t b) { return a b; } API void fill_student(Student* s, int32_t id, const char* name, double score) { s-id id; memset(s-name, 0, sizeof(s-name)); strncpy(s-name, name, sizeof(s-name) - 1); s-score score; }编译要用和 JVM 位数一致的工具链。打开x64 Native Tools Command Prompt for VScl /nologo /LD /O2 /Fe:demo.dll demo.c/LD表示生成 DLL。用 MinGW 的话是gcc -shared -O2 -o demo.dll demo.c。注意如果用 C 编译导出函数会被名字修饰name manglingJava 侧按原名根本找不到所以要么用extern C包一层要么提供.def文件显式指定导出名。3.2 JNA 的接口映射类型对应表是最该背下来的东西引入依赖Maven 版dependency groupIdnet.java.dev.jna/groupId artifactIdjna/artifactId version5.14.0/version /dependencyJava 侧接口长这样import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.Structure; public interface DemoLib extends Library { DemoLib INSTANCE Native.load(demo, DemoLib.class); int add(int a, int b); void fill_student(Student s, int id, String name, double score); }结构体要继承Structure并用Structure.FieldOrder按 C 里的字段顺序声明——顺序错了不会报编译错误只会读到一堆乱码这个坑很阴Structure.FieldOrder({id, name, score}) public class Student extends Structure { public int id; public byte[] name new byte[32]; public double score; public Student() { super(); } public Student(Pointer p) { super(p); read(); } }类型对应关系我整理成表新项目照着抄基本不会错C 类型JNA 类型备注int/int32_tint固定 32 位安全longNativeLongC 的long在 Win 上是 32 位在 Linux 上是 64 位用long会踩坑unsigned intint或NativeLongJava 没有无符号超出范围要自己转float/doublefloat/double直接对应char*输入String默认用平台编码中文环境是 GBKchar*输出缓冲区byte[]或Memory用String接会读到垃圾void*Pointer/Memory需要手动管理生命周期结构体指针Student或Student.ByReference需要传出时用ByReference函数指针继承Callback的接口回调要注意 GC 保活关于编码JNA 默认按jna.encoding或平台默认编码转换字符串。Windows 中文环境下是 GBK如果 DLL 内部用的是 UTF-8 或者宽字符wchar_t直接传String就会乱码。宽字符接口要把WString用上并配合W32APIOptionsDemoLib lib Native.load(demo, DemoLib.class, W32APIOptions.UNICODE_OPTIONS);判断 DLL 用的是窄字符还是宽字符看头文件里是char*还是wchar_t*别猜。我在一个项目上就是因为没注意这点接口调用一直返回空字符串查了两小时才发现是编码问题。3.3 在 IDEA 里跑起来并验证主类就这么几行public class Main { public static void main(String[] args) { int sum DemoLib.INSTANCE.add(3, 4); System.out.println(add sum); Student s new Student(); DemoLib.INSTANCE.fill_student(s, 1001, 张三, 92.5); System.out.println(s.id new String(s.name).trim() s.score); } }把demo.dll放到 Run Configuration 的工作目录或者加-Djava.library.pathD:\libs。跑之前建议加一个绝对可靠的探针System.out.println(library path System.getProperty(java.library.path)); File f new File(D:/libs/demo.dll); System.out.println(exists f.exists() , len f.length());exists为 false 就直接去解决路径问题不用往下查了。4. 手写 JNI 的完整链路从生成头文件到导出函数名4.1 javac -h 生成头文件注意类名即函数名JNI 的函数名规则是Java_ 全限定类名点换成下划线 方法名。所以类名和包名一旦改了C 侧的导出名必须跟着改这是维护成本的主要来源之一。package com.example.nativedemo; public class NativeDemo { static { System.loadLibrary(demo); } public static native int add(int a, int b); public static void main(String[] args) { System.out.println(jni add add(3, 4)); } }JDK 8 以后不需要javah了直接用javac -hjavac -h ./target/jni-headers -d ./target/classes \ src/main/java/com/example/nativedemo/NativeDemo.java会在./target/jni-headers下生成com_example_nativedemo_NativeDemo.h里面声明的函数签名大致是JNIEXPORT jint JNICALL Java_com_example_nativedemo_NativeDemo_add (JNIEnv *, jclass, jint, jint);注意第二个参数是jclass静态方法或jobject实例方法写错了编译能过调用时崩。实现文件#include jni.h #include com_example_nativedemo_NativeDemo.h JNIEXPORT jint JNICALL Java_com_example_nativedemo_NativeDemo_add (JNIEnv *env, jclass clazz, jint a, jint b) { return a b; }4.2 编译链接头文件路径和导出名是两个必错点cl /nologo /LD /O2 ^ /I%JAVA_HOME%\include ^ /I%JAVA_HOME%\include\win32 ^ /Fe:demo.dll demo.cinclude和include\win32Linux 下是include/linux两个路径都要给只给第一个会提示找不到jni_md.h。这是新人最常卡住的地方。如果实现文件是.cpp一定要加extern C { #include com_example_nativedemo_NativeDemo.h }不然 C 编译器会把函数名修饰成?Java_com_example_...YAH...这种形式JVM 按原名字找不到会报UnsatisfiedLinkError: com.example.nativedemo.NativeDemo.add()I。想确认导出名用dumpbin /exports demo.dll看导出的符号列表里有没有你期望的那个名字。这一步花十秒能省掉半小时的困惑。4.3 System.load 和 loadLibrary 到底该用哪个System.load(D:/libs/demo.dll)必须给绝对路径且带扩展名路径写死。System.loadLibrary(demo)只给库名由 JVM 去java.library.path里找扩展名由平台补齐System.mapLibraryName(demo)会返回demo.dll或libdemo.so。第二个更通用但要求你把路径配好。第一个更适合路径在运行期才知道的情况比如从 jar 里解压出来。static {}块里加载是有讲究的它保证在第一次真正用到这个类时才加载 DLL而不是类一被引用就加载。如果 DLL 加载失败抛的是ExceptionInInitializerError包着UnsatisfiedLinkError栈看起来会有点绕别被吓到。还有一个 ClassLoader 层面的坑同一个 DLL 在一个 JVM 里只能被一个 ClassLoader 加载。热部署、多模块、Spring Boot DevTools 重启这类场景很容易撞上Native Library ... already loaded in another classloader。DevTools 的重启机制是把应用类加载器整个换掉旧的那个没被回收DLL 就还挂着。遇到这个问题开发阶段把spring-boot-devtools去掉或者把原生加载逻辑提到一个由系统类加载器加载的类里通常能缓解。5. UnsatisfiedLinkError 到 WinError 1114按顺序反推真实原因5.1 先把报错分成三类别一上来就瞎试我处理这类问题的第一步不是改代码是看错误信息属于哪一类。因为三类错误的原因域完全不同混着猜会无限循环。报错形态含义排查方向no demo in java.library.path根本没找到文件路径、文件名、扩展名、工作目录Cant load IA 32-bit .dll on a AMD 64-bit platform找到了但位数不匹配JVM 位数、DLL 位数找不到指定的模块/The specified module could not be found找到了但它依赖的东西没找到依赖 DLL、VC 运行库、依赖的搜索路径动态链接库(DLL)初始化例程失败/WinError 1114DLL 的 DllMain 执行失败加载顺序、依赖初始化、重复加载、路径冲突already loaded in another classloader同一 DLL 被两次加载类加载器、热部署、多个模块各带一份这张表是我踩过足够多次之后整理出来的照它走能少绕很多路。5.2 1114 这个错误为什么最容易误判oserror: [winerror 1114] 动态链接库(dll)初始化例程失败这个提示在各种语言里都出现过Python 加载 PyTorch 的时候也常看到。它的本质是ERROR_DLL_INIT_FAILED系统调用了这个 DLL 的DllMain(DLL_PROCESS_ATTACH)而DllMain返回了 FALSE或者它在初始化过程中又去加载别的 DLL 失败了。放到 Java 场景里1114 常见于三种情况。第一种是依赖链里的 DLL 初始化失败。比如demo.dll静态依赖third_party.dll而third_party.dll自己也有一段初始化逻辑它依赖的配置文件或硬件没准备好于是整条链断在中间。表现是单独用本地程序加载demo.dll没事从 Java 里加载就 1114。这种差异往往来自工作目录不同——DLL 初始化时按相对路径找配置而 Java 进程的工作目录是项目根目录。第二种是同一个 DLL 被从两个不同路径加载。Windows 的加载器认为同名不同路径是两个模块但如果这个 DLL 内部有全局单例或者共享内存段第二个实例的初始化就会失败报 1114 或者 998访问无效内存。解决方式是在加载前设置搜索目录把加载路径收敛到一个SetDllDirectory(LD:\\libs);或者用LoadLibraryEx配合LOAD_WITH_ALTERED_SEARCH_PATH。Java 侧如果不方便写 C就保证java.library.path里该 DLL 只出现一次并且不要在某些路径下也放一份副本。第三种是位数混装导致的初始化失败。这个比较隐蔽理论上位数不对会在更早的阶段就报错但在依赖链场景下32 位的依赖 DLL 被 64 位进程尝试加载报的可能就是 1114。所以排查时把整条依赖链的位数都确认一遍不只是最外层那个。5.3 一套我常用的排查动作清单遇到加载失败我按这个顺序做基本三次以内能定位打印java.library.path和目标文件的exists()确认路径这一层没问题。用dumpbin /headers或等价工具核对目标 DLL 和 JVM 的位数。用dumpbin /dependents列出直接依赖逐一确认这些依赖是否在搜索路径里。依赖层级深的时候用 Process Monitor 抓Load Image事件直接看它到哪个文件为止没找到。单独写一个最小的 C 程序去加载这个 DLL隔离掉 Java 这一层。如果 C 程序也失败说明问题在 DLL 本身或其环境如果 C 程序成功而 Java 失败问题在 Java 侧的加载路径或工作目录。检查工作目录。把 Run Configuration 的 Working directory 设成 DLL 所在的目录很多时候问题自己就没了。第 5 步是分水岭。很多人一上来就在 Java 里反复试各种加载方式其实先确认DLL 本身在你这台机器上能不能被加载更省时间。6. 从 IDEA 能跑到别人机器上也能跑中间差了什么6.1 把 DLL 塞进资源目录运行期解压再加载-Djava.library.path只能骗过你自己的电脑交付时必须让 DLL 跟着 jar 走。做法是把 DLL 放到src/main/resources/native/启动时复制到临时目录再用绝对路径System.loadpublic final class NativeLoader { public static void load(String libName) { String fileName System.mapLibraryName(libName); // demo.dll try (InputStream in NativeLoader.class .getResourceAsStream(/native/ fileName)) { if (in null) { throw new IllegalStateException(资源中不存在 fileName); } Path dir Files.createTempDirectory(native-); Path target dir.resolve(fileName); Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); target.toFile().deleteOnExit(); System.load(target.toAbsolutePath().toString()); } catch (IOException e) { throw new IllegalStateException(释放本地库失败: fileName, e); } } }三个注意点。一是System.mapLibraryName会帮你处理扩展名差异跨平台时能省事。二是解压出来的文件在本进程存活期间是被锁定的deleteOnExit只是个尽力而为的清理进程异常退出就会留下临时目录。如果你很在意这个可以在启动时顺手清理一下上次残留的目录但要小心别删到正在被引用的文件。三是每次启动都解压到新目录会导致同名 DLL 从不同路径被加载这在某些场景下会触发前面说的 1114。稳定的做法是用固定的临时目录名配合内容哈希做版本区分Path dir Paths.get(System.getProperty(java.io.tmpdir), myapp-native-v3); Files.createDirectories(dir);这样同一个版本永远走同一个路径升级时换目录名即可。6.2 DLL 冲突你以为在调 A其实调的是 BDLL 冲突的典型表现是开发机能跑、测试环境不能跑或者行为诡异但没报错。原因通常是搜索顺序里有个同名 DLL 抢先被加载了。Windows 的搜索顺序大致是进程所在目录、系统目录、PATH里的目录。如果你的程序目录下恰好有一个同名的旧 DLL加载到的就是它。排查手段还是 Process Monitor看它实际打开的是哪个路径的文件。预防手段有两个一是给产物起个不容易撞名的名字比如带上项目前缀二是在 Java 侧显式用绝对路径加载不给搜索顺序留机会。这里必须提一句关于网上下载 DLL 的事。搜索相关错误时你会看到大量DLL 修复DLL 下载的结果甚至能看到各种修复工具。我的建议是不要用。原因很简单DLL 是二进制执行代码你无法验证它的来源和内容把它覆盖到系统目录还可能破坏其他软件。正统做法永远是装官方运行库、让厂商提供完整依赖、或者重新编译。我在客户现场见过因为随手替换系统 DLL导致几台机器的业务系统全部起不来的情况恢复花了大半天。6.3 同时支持 32 位和 64 位 DLL如果厂商两种位数都给了可以按架构分目录存放加载时动态选String arch System.getProperty(sun.arch.data.model); // 32 或 64 String path /native/win- arch / fileName;32 位和 64 位 JVM 各自只会加载自己那份互不干扰。如果厂商只给了 32 位而你的生产环境是 64 位 JVM那前面提到的进程外调用就是唯一务实的出路别指望能在同一个 JVM 里混着来。7. 长期运行才会暴露的几个问题7.1 内存与引用本地内存不归 GC 管这一点最容易被忽略。new byte[1024 * 1024]这种 Java 堆内的数组GC 会管但Memory或者直接用 JNI 分配出来给 DLL 用的缓冲区走的是本地内存Java 堆看不到GC 也不会因为本地内存吃紧而触发。JNA 里Memory提供了dispose()和close()用完就该释放。如果你的循环里每次调用都new Memory(size)跑一晚上进程就会被系统干掉而 JVM 堆看起来一切正常jstat什么都看不出来。排查这种问题要用任务管理器或者jcmd pid VM.native_memory开了 NMT 的前提下看进程的 RSS。另外用String接收 DLL 返回的char*时JNA 会帮你把 C 字符串拷成 Java 字符串只要这个指针指向的内存是 DLL 管理的拷完就跟 Java 无关了不用手动释放。但如果你拿到的是Pointer那就要自己判断所有权谁分配谁释放这个规则必须和 DLL 提供方确认清楚写进注释里。7.2 线程与回调JNIEnv 不能跨线程存JNI 的JNIEnv*是线程绑定的一个线程拿到的JNIEnv不能缓存起来给另一个线程用这是硬规则。如果你写的是 JNI 胶水层需要在别的线程里回调 Java必须用AttachCurrentThread拿到当前线程的JNIEnv并在退出前DetachCurrentThread不然会造成线程泄漏。JNA 里的回调Callback接口有个更隐蔽的问题回调对象必须被 Java 侧持有强引用否则可能被 GC 回收DLL 回调时就是野指针直接崩进程。经验做法是把回调对象挂到一个静态字段或者长生命周期的容器里private static final ListCallback CALLBACK_KEEPALIVE new ArrayList();看起来土但确实是官方文档也推荐的保活手段。用 JNA 的话建议开启受保护模式Native.setProtected(true);开启后DLL 里的非法访问会被转换成 Java 异常而不是直接把 JVM 带走。代价是有一定性能损耗并且并非所有平台的信号都能可靠拦截。生产环境是否开启要权衡我个人的做法是测试环境开着方便定位生产环境关掉以拿到真实的崩溃现场。7.3 崩了怎么定位hs_err_pid 是最有价值的线索原生库把 JVM 带崩的情况最典型的产物是工作目录下多出来的hs_err_pid数字.log。这个文件告诉你的关键信息有几处# Problematic frame崩在哪个模块的哪个函数如果是你的 DLL 名字加上一个地址说明崩在 DLL 里。# C [demo.dll0x1234]偏移地址配合 DLL 的符号表.pdb可以定位到具体函数用dumpbin /disasm或者 WinDbg 都能看。线程栈能看到是从哪个 Java 方法发起的调用。定位崩在 DLL 的哪个函数通常是靠 pdb 符号文件加 WinDbg。这一步已经超出 Java 的范畴了但如果 DLL 是自家 C 团队编的让他们提供 pdb 能极大缩短定位时间。如果 DLL 是第三方提供的那就退一步用最小复现脚本把触发崩溃的输入记录下来交给厂商。还有一个实用技巧给崩溃现场做日志留存。在调用 DLL 之前和之后各打一行带参数的日志用 try-catch 兜住 Java 异常虽然兜不住原生崩溃但能让你知道崩溃发生在哪一次调用、参数是什么。这个日志在事后复盘时价值极高。8. 我在几个真实项目里攒下的小经验有个细节值得单独说System.load和System.loadLibrary的调用顺序是有意义的。如果demo.dll依赖base.dll建议先System.load那个base.dll再加载demo.dll。这样即使base.dll的路径不在搜索目录里它也已经驻留在进程里demo.dll的加载就能成功。这个技巧在依赖目录结构比较乱的时候特别好用也避开了设置PATH要重启 JVM 的麻烦。另一个经验是关于调试的。IDEA 里调试跨原生调用的代码Java 侧的断点当然能用但一旦进入原生函数IDE 就帮不上忙了。我的做法是在 Java 侧把参数完整打印出来然后用同样的参数写一个最小的 C 程序去调这个 DLL把问题定位在哪一侧。听上去笨但比我见过的任何高级方法都快。另外IDEA 的 Run Configuration 里可以把Working directory设成一个专门放 DLL 的目录这样程序启动时进程目录就是 DLL 所在目录很多加载失败会自然消失。还有一点关于版本管理把 DLL 一起提交到仓库。我见过太多团队把 DLL 放在共享盘或者个人电脑上某天换人接手或者换机器项目就再也跑不起来了。DLL 是二进制但它和源码同等重要。如果 DLL 很大用 Git LFS 也行但一定要有版本记录并且在文件名或者目录名里体现版本号和位数比如native/win-x64/demo-2.3.1.dll一眼就能看出用的是哪一份。最后是一个关于接口文档的建议。拿到 DLL 之后先花时间把 Java 侧的接口声明整理成一份自己的文档函数名、参数类型、返回值含义、字符串编码、内存所有权、线程安全性。这份文档在后续维护中的价值远超当初写它花的那两个小时。尤其是内存所有权这一条DLL 到底期望调用方分配缓冲区还是由它返回指针、由谁释放很多头文件里根本不写只能问或者看示例代码。这类信息如果只存在某个人的记忆里项目迟早会出问题。
返回列表