一. JNI简介
JNI是Java Native Interface的缩写, 它是一套实现了Java和其他语言通信(主要是C&C++)的的接口.
如果在不同的平台, java程序可以通过jvm成功运行, 但是一旦使用了JNI, 就会丧失这种平台移植性, 简单说就是如果有一套基于某个平台(如Windows 64位)的C语言程序, 然后使用JNI让其与一个java程序绑定, 那么这个java程序就只能跑在64位的windows系统上了. 在32位的windows, linux和android以及其他平台都用不了了. 即使强行编译过然后再其他平台跑起来了, 执行到native方法的时候也会报错.
二. 文章目的以及学习方向
本文主要用于帮助初次接触JNI的同学成功编写一个JNI的Hello World程序.
由于JNI是一个很深入的知识, 每个平台编译动态库的方式也不一样, 笔者也是刚开始接触这方面的知识, 所以就先从最基础的Windows平台的动态库开始学习(也是因为使用起来很简单, 不需要其他设备, 一台电脑即可完成调试), 然后将JNI的基本知识了解一些以后 (也顺便提升一下大学以后就基本没用过的C语言的水平), 再开始进行Android平台的JNI编程.
三. 环境搭建
Intellij IDEA, JDK11, MinGW64, Win10 64位.
四. 步骤详解(笔者每一步都进行了一次提交, 以便于理解流程)
1. 创建一个新的Java工程, 并创建主类. (Init Win10 64bit JNI demo.)
2. 创建一个类用于声明JNI方法并在主类中进行调用, 这里由于只要实现Hello World功能, 所以无需参数和返回值.(Create class, declare methods and invoke them.)
3. 使用jdk生成与声明对应的JNI lib的头文件(Build header file):
1. jdk version <= 8:
首先将声明jni方法的java文件编译出对应的class文件, 在我的demo中, 就是NativeMethods.java
随后执行下面的语句
javah -jni -cp out\production\Win64JNI\ -d jni\ com.jayson.jni.windows.NativeMethods
执行结束后, 就可以在Win64JNI\jni\下看到头文件了.
命令详解
-jni: 说明是要使用jni的模式
-cp: 指定classpath
-d: 文件输出路径
注意: 最后指明需要编译的头文件的全类名, 由于使用的是idea执行build生成的.class文件, 所以文件路径其实为 Win64JNI\out\production\Win64JNI\com\jayson\jni\windows\NativeMethods.class. 配置classpath参数的时候, 配置到工程名即可, 不要进入到包名下, 如果classpath设置为out\production\Win64JNI\com\ 或是其他的, 会导致生成头文件失败.
2. jdk version >= 10:
由于jdk10以上, 将javah工具集成进了java中, 只需要配置命令即可使用功能, 并且将编译与生成头文件合并了, 一行命令完成了编译java文件为class和将class生成对应的头文件两个操作.
javac -h jni\ -d out\classes\ src\com\jayson\jni\windows\NativeMethods.java
执行结束后, 就可以在Win64JNI\jni\ 路径下看到生成的头文件了.
命令详解
-h: 表示要使用将javac编译出来的.class文件生成对应的.h头文件, 并指定.h文件的输出路径
-d: 指定.class文件的输出路径
注意:
1. 笔者直接使用的idea的Terminal命令窗口进行的操作, 也就是所有操作的根路径都是我项目的根路径:E:\Projects\Study\Win64JNI>
2. 笔者的java环境配置的是jdk11, 所以没有准备jdk8的截图.
4. 创建JNI方法的具体实现(Implement jni method):
1. 在Win64JNI\jni\ 路径下创建一个C文件 jayson.c, 并添加需要用到的头文件.
2. 将头文件中的方法声明复制到jayson.c中并进行实现.
这里有几个细节要注意:
1. 由于只是实现Hello World功能, 所以JNIEnv和jclass, jobject三个参数类型暂时不需要了解, 只要知道它们将来会有用就好了.
2. 头文件的方法声明最后有个分号, 如果不注意就在最后添加方法体, 那么最后很可能不是添加了方法体而是仅仅添加了一个代码块, 所以这里要细心的把那个分号删除掉.
3. 头文件的方法声明只有形参的类型, 没有形参的变量名以及方法体, 所以需要手动增加形参名称和方法体.
5. 编译生成windows 64bit对应的动态链接库(Build dll)
执行命令:
gcc -I"D:\Develop\Environment\jdk-11.0.3\include" -I"D:\Develop\Environment\jdk-11.0.3\include\win32" -shared -o jni\jni_lib.dll jni\jayson.c
然后就可以在 Win64JNI\jni\ 目录下看到 jni_lib.dll 了
命令参数详解:
-I: 表示将D:\Develop\Environment\jdk-11.0.3\include目录作为第一个寻找头文件的目录,寻找的顺序是: D:\Develop\Environment\jdk-11.0.3\include-->/usr/include-->/usr/local/includ.
-shared: 标识要生成共享库, 这里也就是指动态库.
-o: 输出文件的路径, 上面的命令标识编译输出为 Win64JNI\jni\ 路径下的 jni_lib.dll 文件.
6. 执行主方法
由于笔者使用的是IDEA直接执行main方法, 没有使用命令行, 所以执行和配置都按着IDEA环境来.
如果直接执行main方法, 会报出一个错误UnsatisfiedLinkError, 这个错误的意思是动态库链接失败, 也就是没有找到咱们生成的jni_lib.dll, 这时候需要手动配置一下, 在IDEA的菜单栏找按如下顺序点开对话框 Run -> Edit Configurations... , 然后在VM options那一栏添加放置动态库的路径, 如示例工程则是-Djava.library.path=E:\Projects\Study\Win64JNI\jni. 这时候再去执行一下main方法, 即可成功输出在c文件中打印的日志了.
五. 案例项目地址
六. 我遇到的一些问题
1. 运行后报错: 不是有效的win32程序:
一开始查找资料都说卸载sdk后就可以解决问题, 但是我尝试了这种方法后发现问题仍然存在, 所以分析问题可能出现的原因.
由于代码原本是可以执行的, 但是经过某次操作我卸载了1.8的jdk换成jdk10以后重新生成动态库, 就出现了这个问题.
所以定位到这个问题是由于动态库与环境不匹配导致的, 不需要大张旗鼓的修改环境变量等. 由于咱们使用的是MinGW64, 支持交叉编译, 所以只需要在编译命令上加上 -m64 参数即可, 示例如下:
gcc -m64 -I"D:\DevEnvironment\Java\jdk_10\include" -I"D:\DevEnvironment\Java\jdk_10\include\win32" -shared -o jni\Jayson.dll jni\JaysonJni.c
另外, 如果是想编出来32位的动态库, 那么编译命令则是加上 -m32.
2. 卸载jdk后命令行输入java -verison仍然显示原来的版本
这个问题是由于没有卸载干净引起的, 手动删除掉C:\Windows\System32\ 目录下的java.exe, javaw.exe, javaws.exe三个文件即可.
问题产生的原因: 在安装新的JDK时, 会将java.exe, javaw.exe, javaws.exe三个可执行文件复制到了C:\Windows\System32目录, 这个目录在WINDOWS环境变量中的优先级高于JAVA_HOME设置的环境变量优先级, 所以要将这个目录中这三个文件删除.
如果没有在C:\Windows\System32目录下找到对应的文件, 那么文件可能会是在C:\ProgramData\Oracle\Java\javapath目录下, 在这个目录下找到对应的删除掉即可.