通常,在开发 JNI 程序时,我们都会使用 IDE,例如 Eclipse , Android Studio , 这是因为工具简化了开发的流程,提升了工作效率,但是却让我们越来越看不清本质的东西。因此,本篇文章就不使用 IDE 来做一次 JNI 开发,这样我们就可以对原生 JNI 有个更全面的了解。
本文的例子是在 Ubuntu 16.04 下运行的。
首先我们新建一个项目目录叫 JNIDemo
,然后进入这个目录
bxll:~$ mkdir JNIDemo bxll:~$ cd JNIDemo/ 复制代码
在 JNIDemo
目录下,新建一个叫 Hello.java
的文件,代码内容如下
package com.bxll.jnidemo; public class Hello { static { System.loadLibrary("hello_jni"); } static native String helloFromJNI(); public static void main(String[] args) { System.out.println(helloFromJNI()); } } 复制代码
首先,在静态代码块中,通过 System.loadLibrary()
方法加载一个名为 hello_jni
的库,在 Linux
平台下,这个库的全名叫做 libhello_jni.so
,在 Windows
平台下,这个库的名字叫做 hello_jni.dll
。由于我使用的是 Ubuntu
,因此一会编译这个库的名字就必须为 libhello_jni.so
。
然后,定义了一个 native
方法 helloFromJNI()
,这个方法需要在动态库中实现,这个稍后就会看到。
最后,在 main()
方法中,调用这个 native
方法,并输出这个方法的返回结果。
在编译 Hello.java
文件之前,我们需要创建一个存放字节码文件的目录,这个目录暂且就叫做 classes
吧
bxll:~/JNIDemo$ mkdir classes 复制代码
然后,我们把编译生成的字节码文件输出到这个目录
bxll:~/JNIDemo$ javac -d classes/ Hello.java 复制代码
javac
的 -d
参数表示输出的目录,更多参数请参考javac 。
在执行生成头文件操作之前,我们必须要搞清楚一个问题,那就是为何要生成头文件? 因为头文件中声明的函数和 Java
文件中声明的 native
方法是一一对应的关系,虚拟机会自动帮我们建立这层联系,这也称之为静态注册。
在生成头文件之前,我们需要创建一个存放头文件的目录 jni
bxll:~/JNIDemo$ mkdir jni 复制代码
然后把生成头文件,并指定目录为 jni
bxll:~/JNIDemo$ javah -classpath classes/ -d jni/ com.bxll.jnidemo.Hello 复制代码
javah
的 -classpath
参数指定字节码的目录,就是我们刚才编译文件所指定的目录, -d
参数指定头文件生成的目录,最后的 com.bxll.jnidemo.Hello
指定字节码文件的全路径。更多的 javah
命令参数请参考javah 。
ok, 现在头文件生成了,我们现在来看看它的内容吧,简化版的内容如下
// com_bxll_jnidemo_Hello.h /* * Class: com_bxll_jnidemo_Hello * Method: helloFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI (JNIEnv *, jclass); 复制代码
我们只需要关心这个函数原型(其他都是C/C++相关的事),因为 Java_com_bxll_jnidemo_Hello_helloFromJNI
这个函数就是对应的 Hello.java
中的 native
方法 helloFromJNI()
。
另外呢,我们还可以看到有三行注释,第一个注释 Class: com_bxll_jnidemo_Hello
指明了这个函数与哪个 Java
类 相关,第二个注释 helloFromJNI
指明是实现了哪个 native
方法,第三个参数 ()Ljava/lang/String;
代表 Java
类的 native
方法在 JNI
中的签名。
JNIEXPORT
和 JNICALL
都是宏,因为不同平台调用动态库中的方法有不同的规范,而这两个宏就是为了做兼容处理,在 Linux
平台,这两个宏其实没有什么用,因为这两个宏都是定义为空的。
既然从头文件中已经了解到函数原型,那么就好实现了
// com_bxll_jnidemo_Hello.cpp #include "com_bxll_jnidemo_Hello.h" extern "C" JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI (JNIEnv * env, jclass clazz) { const char * str_hello = "Hello from C++"; return env->NewStringUTF(str_hello); } 复制代码
这里涉及到 JNI
如何生成字符串的,这里暂不做详述。
既然我们已经实现了底层函数,就需要将这些打包成库,以方便 Java
层加载。我们选择把源文件打包成动态库,但是在执行这个动作之前,我们必须保证操作系统的 Java
开发环境已经部署妥当,最好也设置了 JAVA_HOME
环境变量,先看下我的 JAVA_HOME
环境变量
bxll:~/JNIDemo$ echo $JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/ 复制代码
那么,现在来生成动态库吧
bxll:~/JNIDemo$ g++ -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -fPIC -shared jni/com_bxll_jnidemo_Hello.cpp -o jni/libhello_jni.so 复制代码
g++
的 -I
参数指明 JNI
头文件的位置, -fPIC
表示变成成与位置无关的独立代码, -shared
表示编译成动态库, -o
指明生成动态库的目录以及名字,在 Linux
系统下,动态库的名字的形式为 libXXX.so
。
现在动态库都生成了,那么可以运行程序了吗?我们试一下
bxll:~/JNIDemo$ java -classpath classes/ com.bxll.jnidemo.Hello Exception in thread "main" java.lang.UnsatisfiedLinkError: no hello_jni in java.library.path at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867) at java.lang.Runtime.loadLibrary0(Runtime.java:870) at java.lang.System.loadLibrary(System.java:1122) at com.bxll.jnidemo.Hello.<clinit>(Hello.java:6) 复制代码
java.lang.UnsatisfiedLinkError
就是告诉你动态库没有链接上,并且后面有说明原因 no hello_jni in java.library.path
,告诉你 java.library.path
没有发现名为 hello_jni
的动态库,在 Linux
平台下,也就是没有发现 libhello_jni.so
库。
既然我们已经知道原因是在 java.library.path
属性所指定的目录下没有找到库,那么我可以把生成的库放到这个指定路径下,这样就可以了吧。没错,确实可以,但是这样未免太麻烦,在 Linux
平台下,可以把库的路径加入到 LD_LIBRARY_PATH
环境变量中,程序也会在这个路径下搜索库。
由于我的开发环境中暂时还没有定制自己的库,因此 LD_LIBRARY_PATH
这个环境变量为空,那么现在我们设置下
bxll:~/JNIDemo$export LD_LIBRARY_PATH=./jni/ 复制代码
我们把库的搜索路径指定到了当前目录下的 jni
目录,因为刚才我们把动态库输出到这个目录下。
那么,现在再运行这个 Java
程序,你就会看到想要的效果
bxll:~/JNIDemo$ java -classpath classes/ com.bxll.jnidemo.Hello Hello from C++ 复制代码
结果已经说明一切。