之前完全没有接触过JNI(java native interface),最近在研究apk增量更新的时候通过博客 http://blog.csdn.net/sgwhp/article/details/8872941 和 http://blog.csdn.net/sgwhp/article/details/9009427 这两篇博客了解到了JNI的相关知识,在研究过程中遇到诸多问题,最后总算研究出一些名堂,在这里总结一下,希望能给这方面的新手提供一些帮助。
JNI全称Java Navtive Interface,它提供了若干的API来实现Java和其他语言的通信(主要是C&C++),当需要在Java程序中调用c,c++代码的时候,就要用到JNI了。
JNI编程大概分成以下几步:
- native接口定义
- 用javah命令生成.h头文件
- 在.c或.cpp文件中实现.h中定义的方法
- 将.h文件和.c或.cpp文件编译成.dll或者.so文件,其中用于.dl在lwindows运行环境中调用,.so用于linux/android运行环境调用(linux和android的so文件无法共用,需要分别编译,编译的方法和环境不相同,后面会介绍)
- 在Java代码中通过System.loadLibrary或System.load将dll/so动态库加载到内存,然后调用native方法
下面是具体步骤:
1.JNI接口定义:
package com.xiyuan.util;
public class JniUtil {
public native String hello();
}
2.通过javah编译出一个.h头文件
打开cmd(windows)或者Linux终端,定位到该JNI类所在的src目录下,然后运行命令javah 包名.l类名,即可在当前目录编译出一个.h文件
那上面的接口举个例子,首先JniUtil这个java文件在.../JniDemo/src/com/xiyuan/util/目录下,我们需要在命令行中定位到.../JniDemo/src/目录下,然后运行javah com.xiyuan.util.JniUtil,即可生成.h头文件,生成的文件为com_xiyuan_util_JniUtil.h
3.实现.h中定义的方法
先看一下.h中的内容
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_xiyuan_util_JniUtil */
#ifndef _Included_com_xiyuan_util_JniUtil
#define _Included_com_xiyuan_util_JniUtil
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_xiyuan_util_JniUtil
* Method: hello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_xiyuan_util_JniUtil_hello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
这里面定义了一个JNIEXPORT jstring JNICALL Java_com_xiyuan_util_JniUtil_hello(JNIEnv *, jobject)方法,下面就要在.c或.cpp文件中来实现这个方法。 Linux下面在.c文件中编写实现代码,windows下面可以用vs2013等开发工具创建一个win32项目,然后将生成文件类型设置为dll就可以了
Linux下面的com_xiyuan_util_JniUtil.c文件
#include <string.h>
#include <jni.h>
#include <com_xiyuan_util_JniUtil.h>
jstring Java_com_xiyuan_util_JniUtil_hello
(JNIEnv* env, jobject job)
{
return (*env)->NewStringUTF(env, "Hello from JNI !");
}
Windows下面的com_xiyuan_util_JniUtil.cpp文件
#include "string.h"
#include "com_xiyuan_util_JniUtil.h"
jstring JNICALL Java_com_xiyuan_util_JniUtil_hello
(JNIEnv* env, jobject job)
{
return env->NewStringUTF("Hello from JNI !");
}
这两个的内容有一些差别,需要注意下。另外,在Linux下面,只要com_xiyuan_util_JniUtil.h和com_xiyuan_util_JniUtil.c在同一个文件夹下面就可以了正常编译了,但是在windows下面的vs2013中编译的时候提示找不到jin.h,解决办法就是把jdk安装目录下的.../include/jni.h和.../include/win32/jni_md.h这两个文件复制到项目里面,和com_xiyuan_util_JniUtil.h放在一起
在vs2013中创建这个项目的示例:
项目创建完成之后,把jdk安装目录下的.../include/jni.h和.../include/win32/jni_md.h以及生成的.h文件(我这个例子就是com_xiyuan_util_JniUtil.h)放在项目的根目录下和项目同名的文件夹中,我这个例子中,项目的跟路径如下:
我们要把三个头文件放到JniUtil文件夹下
然后在vs2013里面在头文件上面右键添加现有项,把三个.h头文件都添加进来:
然后在源文件上面右键添加新建项
创建一个新的cpp文件,文件名和之前生成的.h头文件保持一致
最后的撞见好的win32项目如下
然后编写.cpp文件实现.h重定义的方法,代码在前面已经贴出来了。这时候com_xiyuan_util_JniUtil.h中报错提示无法打开与那文件jni.h,只要把#include <jni.h>改成#include "jni.h"就可以了。
4.编译生成动态库(dll或者so文件)
生成so文件,我实在Linux虚拟机中进行的,在终端中定位到.h和.c文件所在目录
运行下面的命令:
gcc -I/usr/local/jdk1.7.0_45/include/linux -I/usr/local/jdk1.7.0_45/include -I/home/xiyuan/eclipse/JniDemo/jni -fPIC -shared -o libJniUtil.so com_xiyuan_util_JniUtil.c
其中/usr/java/jdk1.7.0_45/include/linux/和/usr/java/jdk1.7.0_45/include/根据你的Linux中的jdk安装情况来修改,/home/xiyuan/workplace/JniUtil/jni改成.c和.h文件所在的路径,libJniUtil.so是编译出来的文件的名字,com_xiyuan_jni_JniUtil.c是参与编译的.c文件,可以写好几个,.h文件不用写,运行后在当前目录生成一个so文件
生成dll文件,在第三步中阐述了在vs2013中创建项目和添加资源文件的过程,现在就需要编译这个项目生成dll文件。这里需要说明一下,Linux下生成的so文件只能在Linux下调用,vs2013编译的dll只能在windows环境下调用,android开发中用ndk生成的so文件只能在android项目中调用,不能在Linux中正确调用,而且在Windows下面还分32位和64位,64位操作系统无发调用32位的dll动态库。
vs2013可以通过设置编译选项编译出32位和64位的dll动态库:
生成32位dll的配置(默认的配置):
生成64位的dll的配置
之后再项目上右键生成
编译成功之后
如果用32位配置编译,则在项目根目录下的Debug目录中能找到生成的dll文件
如果用64位配置编译,则在项目根目录下的x64目录下的Debug目录中能找到生成的dll文件
生成的dll文件的名字和项目名是一致的,为了和so文件命名方式保持一致,将JniUtil.dll重命名为libJniUtil.dll
到这里,动态库生成完毕了,接下来就可以在Java代码中调用了
5.导入动态库并调用
将dll或so文件复制到定义native接口的java项目中,放到一个source folder下面(也可以直接放到src目录下面,我是另外创建了一个source folder,这样可以保证动态库在项目的根目录下面,打包成Jar包后也能在根目录下)
在调用JniUtil中的native方法之前必须要先将dll或so导入到内存中,一般的做法就是
package com.xiyuan.jni;
public class JniUtil {
private native String hello();
static
{
System.loadLibrary("JniUtil");
}
public String sayHello()
{
return hello();
}
public static void main(String[] args) {
System.out.println(new JniUtil().sayHello());
}
}
注意System.loadLibrary("JniUtil"),这里把前缀lib和后缀.dll(或.so)去掉,直接运行发现报错,报错信息如下
Exception in thread "main" java.lang.UnsatisfiedLinkError: no JniUtil in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1886)
at java.lang.Runtime.loadLibrary0(Runtime.java:849)
at java.lang.System.loadLibrary(System.java:1088)
at com.xiyuan.jni.JniUtil.<clinit>(JniUtil.java:9)
这个异常是指jvm虚拟机在java.library.path指定的目录中没有找到JniUtil这个动态库,那么java.library.path的值是什么呢?通过
System.out.println(System.getProperty("java.library.path"));
将
java.library.path的值打印出来,我的打印出来是/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib,于是把libJniUtil.so复制到其中一个路径下面,例如/lib64,然后程序就能运行成功了,控制台成功打印了一句话hello to C !,这就是在.c文件中实现的方法返回的字符串(经过实际操作,linux下有效,windows下面还是报错)
jstring Java_com_xiyuan_jni_JniUtil_hello
(JNIEnv* env, jobject job)
{
return (*env)->NewStringUTF(env, "hello to C !");
}
那么有没有其他办法呢?当然有。这里先将之前复制到java.library.path目录的so文件删除,否则无法确认后来的操作是否生效。删除后,重新运行程序,报之前的错误了。接下来,右键项目,Build Path -> Configure Build Path
确认后,重新运行程序,能正常运行了(经过实际操作,linux下有效,windows下面还是报错)。
但是,到这里还没有完,如果把这个项目打包成jar包之后放到其他项目进行调用,你会发现,又会报之前的错误,只能按照上面的两种方法来解决问题,但是这样的话jar包的使用就会很麻烦,必须要解决这个问题提高jar的可用性。有两种方案可行,一种是通过反射机制设置java.library.path的值,将so文件所在位置添加到其中
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(
"Failedto get permissions to set library path");
} catch (NoSuchFieldException e) {
throw new IOException(
"Failedto get field handle to set library path");
}
}
在System.loadLibrary("JniUtil")之前通过addLibraryDir将so所在的绝对路径添加到java.library.path中,在Linux下能够正确运行了,但是相同的代码在windows任然无法正常运行,那么有没有一个可以在windows和linux都可以运行的方法呢?经过一番研究,最终我研究出一个方案,在JniUtil打包成jar包之前之后,在windows、linux都可以正常运行的方案,其核心步骤在于通过System.load来加载动态库,那么它和System.loadLibrary有什么区别呢?
1.System.load通过绝对路径加载,例如System.load("D:/project/libJniUtil.dll"),而System.loadLibrary("JniUtil")自动在java.library.path所定义的路径中自动查找动态库并加载libJniUtil.so
2.如果动态库a依赖动态库b,那么用System.loadLibrary加载a的时候会自动加载b,而System.load只会加载a,需要提前手动加载b
除了使用System.load来加载,还需要解决一个问题,就是打包成jar之后,绝对路径无法定位到jar包内部的dll或so文件,所以需要在加载动态库之前从jar包之前,需要从jar中将so,dll文件提取出来,然后再加载。
下面是最终的版本:
package com.xiyuan.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class JniUtil {
public native String hello();
private static boolean isLoaded = false;
public JniUtil()
{
if(!isLoaded)
{
isLoaded = true;
String suffx = "";
//根据运行环境选择要加载的动态库
String osType = System.getProperty("os.name").toLowerCase();
if(osType.indexOf("windows") > -1)
{
suffx = ".dll";
}
else if(osType.indexOf("linux") > -1)
{
suffx = ".so";
}
//获取jar包所在的绝对路径
String jarPath = this.getClass().getProtectionDomain().getCodeSource().getLocation().toString();
String jarFilePath = "";
for(String str: jarPath.split("/"))
{
if(str.indexOf("file:") > -1 || str.indexOf(".jar") > -1)
{
continue;
}
jarFilePath += "/" + str;
}
//在jar包所在目录查找动态库文件
String soFilePath = jarFilePath + "/libJniUtil" + suffx;
InputStream in = null;
File soFile = null;
FileOutputStream out = null;
try {
soFile = new File(soFilePath);
if(soFile.exists())
{
//动态库文件已存在,直接加载
System.load(soFilePath);
return;
}
//动态库文件不存在,新建,并从jar中提取动态库文件内容,并输出到新建的动态库文件中
soFile.createNewFile();
in = this.getClass().getClassLoader().getResourceAsStream("libJniUtil" + suffx);
out = new FileOutputStream(soFile);
int len=-1;
byte[] bt = new byte[1024];
while((len = in.read(bt)) != -1) {
out.write(bt,0,len);
}
out.flush();
} catch (IOException e) {
isLoaded = false;
soFile = null;
e.printStackTrace();
}
finally
{
try {
if(in != null)
{
in.close();
}
if(out != null)
{
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
if(soFile != null)
{
System.load(soFile.getAbsolutePath());
}
}
}
}
现在在windows和linux下面都可以正常运行了,部署到百度bae上也能正常工作,并且打包成jar之后在其他项目中也能直接调用运行,不用去担心动态库的路径问题。只是要注意一个问题,在打jar包的时候,一定要保证动态库文件包含在其中了,并且打包之后动态库能够存在于jar包结构的根目录下,之前已经提到把动态库所在文件夹设置为source folder就可以实现这个效果。
最终的demo下载地址http://download.csdn.net/detail/u012570590/8952751