JNI入门教程

之前完全没有接触过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编程大概分成以下几步:

  1. native接口定义
  2. 用javah命令生成.h头文件
  3. 在.c或.cpp文件中实现.h中定义的方法
  4. 将.h文件和.c或.cpp文件编译成.dll或者.so文件,其中用于.dl在lwindows运行环境中调用,.so用于linux/android运行环境调用(linux和android的so文件无法共用,需要分别编译,编译的方法和环境不相同,后面会介绍)
  5. 在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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值