JNI,即Java Native Interface,字面意思“Java本地接口”,这里的本地接口,指的就是c/c++开发的接口。由于JNI是JVM规范的一部分,因此我们写的JNI程序可以在任何实现了JNI规范的Java虚拟机中运行(跨平台)。这里先粗略了解一下Java的JNI开发,为学习Android NDK开发做准备。
JNI开发流程的主要步骤:
下面将以一个HelloWorld的实例来说明上述过程
本机环境macOs High Sierra 10.13,编辑器IDE采用的是IntelliJ IDEA 2016.3.7
HelloWorld.java
package com.rainmonth;
import java.io.IOException;
import static com.rainmonth.Utils.addLibraryDir;
public class HelloWorld {
public static void main(String[] args) throws IOException {
}
public native String sayHello(String name);
}
上面定义了一个native方法sayHello
javac -d out/production/JniLearn/ src/com/rainmonth/HelloWorld.java
运行上面命令,即可在项目根目录的bin/com/rainmonth生成HelloWorld.class文件注意:bin如果之前不存在,需要手动创建,不然会提示找不到目录:bin
温馨提示:可以直接输入javac查看该命令的使用帮助
这一步需要用到javah命令,先看看该命令的用法,终端输入javah
用法:
javah [options]
其中, [options] 包括:
-o 输出文件 (只能使用 -d 或 -o 之一)
-d 输出目录
-v -verbose 启用详细输出
-h --help -? 输出此消息
-version 输出版本信息
-jni 生成 JNI 样式的标头文件 (默认值)
-force 始终写入输出文件
-classpath 从中加载类的路径
-cp 从中加载类的路径
-bootclasspath 从中加载引导类的路径
是使用其全限定名称指定的
(例如, java.lang.Object)。
所以如果你想在根目录的lib下生成HelloWorld.h文件,输入
javah -o lib/HelloWorld.h -classpath bin/ com.rainmonth.Hello
输入后lib目录下得到文件HelloWorld.h
如果你想采用默认的命名方式,只想输出到lib目录,输入
javah -d lib/ -classpath bin/ com.rainmonth.HelloWorld
输入后lib目录下得到文件com_rainmonth_HelloWorld.h(这是该命令默认生成文件名的命名方式)
注意:上面的-classpath是用来指定存放.class文件的目录的,.class文件在输入是不用带后缀
这里采用c实现(也可采用c++实现,二者稍许不同),在lib目录下新建文件HelloWorld.c
// HelloWorld.c
#include "HelloWorld.h"
#ifdef __cplusplus
extern "C"
{
#endif
/*
* Class: com_rainmonth_HelloWorld
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_rainmonth_HelloWorld_sayHello
(JNIEnv *, jobject, jstring)
{
const char *c_str = NULL;
char buff[128] = { 0 };
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL)
{
printf("out of memory.\n");
return NULL;
}
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
printf("Java Str:%s\n", c_str);
sprintf(buff, "hello %s", c_str);
return (*env)->NewStringUTF(env, buff);
}
#ifdef __cplusplus
}
#endif
这一步要用到gcc命令,
上面已经说到了,不同的系统对应的生成命令不同
Mac OS
gcc
-dynamiclib
-o lib/libHelloWorld.dylib lib/HelloWorld.c
-framework JavaVM
-I /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/include/
-I /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/include/darwin/
注意,这里只是为了说明才这样写的,实际上命令见用空格隔开即可( -I甚至都不用空格)。
执行完毕后,lib目录下就得到了libHelloWorld.dylib文件,这就是我们的动态库。
命令参数讲解
-dynamiclib:表示要生存动态库
-o:表示要生成的库文件的名字
lib/HelloWorld.c:指的是实现代码对应的文件
-I:表示生成动态库所要引用的头文件(第一个是平台无关的,第二个是平台相关的,可以打开对应文件看看里面到底是什么鬼。
Windows
该平台我尚未尝试,这里贴上别人的说明(以Windows7下VS2012为例)
开始菜单–>所有程序–>Microsoft Visual Studio 2012–>打开VS2012 X64本机工具命令提示,用cl命令编译成dll动态库:
cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -LD HelloWorld.c -FeHelloWorld.dll
其中,JAVA_HOME为jdk的安装路径,其他参数选项说明如下:
-I : 和mac os x一样,包含编译JNI必要的头文件
-LD:标识将指定的文件编译成动态链接库
-Fe:指定编译后生成的动态链接库的路径及文件名
Linux/Unix
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -shared HelloWorld.c -o lib/libHelloWorld.so
参数说明:
-I: 包含编译JNI必要的头文件
-fPIC: 编译成与位置无关的独立代码
-shared:编译成动态库
-o: 指定编译后动态库生成的路径和文件名
将第一步中的HelloWorld.java修改如下:
package com.rainmonth;
import java.io.IOException;
public class HelloWorld {
static {// 采用静态代码引入动态库
System.loadLibrary("HelloWorld");
}
public static void main(String[] args) throws IOException {
HelloWorld helloWorld = new HelloWorld();
System.out.println(helloWorld.sayHello("Hello"));
}
public native String sayHello(String name);
}
运行结果:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorld in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1865)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at com.rainmonth.HelloWorld.(HelloWorld.java:8)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:123)
意思是,在java.library.path中找不到对应动态库,也就是说动态库的位置可能存在问题。这稍后再解决,我们换一种方式引入动态库,采用绝对路径来引入试试:
package com.rainmonth;
import java.io.IOException;
public class HelloWorld {
static {// 采用静态代码引入动态库
// System.loadLibrary("HelloWorld");
System.load("/Users/RandyZhang/IdeaProjects/JniLearn/lib/libHelloWorld.dylib");
}
public static void main(String[] args) throws IOException {
HelloWorld helloWorld = new HelloWorld();
System.out.println(helloWorld.sayHello("Hello"));
}
public native String sayHello(String name);
}
得到了我们想要的结果
hello Hello
Java Str:Hello
那面上面抛出异常是什么原因引起的呢,然来在java.library.path下找不到我们的动态库文件,这时我们只要将生成的库文件复制到任意一个java.library.path下即可(这个java.library.path可以通过如下方式获取)
public static void main(String[] args) {
System.out.println(System.getProperty("java.library.path"));
}
那很快我们就想到了用System.setProperty(“java.library.path”, pathname)来动态设置这个值,很抱歉,在下尝试了,并没有成功,看了相关的代码你会发现,这个值只在JVM启动时初始化一次:
if (sys_paths == null) {
usr_paths = initializePath("java.library.path");
sys_paths = initializePath("sun.boot.library.path");
}
你后面通过set的方式没有生效,当然有大神已经通过反射的方式解决了这一问题:
public static void addLibraryDir(String libraryPath) throws IOException {
try {
Field field = ClassLoader.class.getDeclaredField("usr_paths");
field.setAccessible(true);
String[] paths = (String[]) field.get(null);
for (int i = 0; i < paths.length; i++) {
if (libraryPath.equals(paths[i])) {
return;
}
}
String[] tmp = new String[paths.length + 1];
System.arraycopy(paths, 0, tmp, 0, paths.length);
tmp[paths.length] = libraryPath;
field.set(null, tmp);
} catch (IllegalAccessException e) {
throw new IOException("Failed to get permissions to set library path");
} catch (NoSuchFieldException e) {
throw new IOException("Failed to get field handle to set library path");
}
}
通过调用这个方法动态的添加库目录到usr_paths,当然局限性就是若表示库目录的变量(即usr_paths)修改了,这种方式就失效了。
我们可以通过配置vmOptions来动态制定java.library.path的值,如:
-Djava.library.path="lib库目录"
具体位置就在Edit Configurations下,或者直接运行命令也可:
java -Djava.library.path=/Users/RandyZhang/IdeaProjects/JniLearn/lib -classpath ./bin/ com.rainmonth.HelloWorld