JNI 入门(一):从Hello World开始

前言

最近在学习JNI的相关知识,即Java Native Interface,它提供了若干API使得Java和C/C++的通信成为可能。我们知道,Java代码运行于Java虚拟机中,独立于某个平台,这也是Java的可移植性的优点。而C/C++代码运行于Windows或Linux平台。为了实现Java和其他代码的交互,JNI应运而生。最简单的就是,就是你在java中声明一个方法,但方法的具体实现是由C/C++代码来实现的,然后生成dll库或者so库,java层通过引入这个库而调用这个Native方法,也就是我们在Android中会遇到的Native方法。

JNI的优点
1、JNI能调用本地操作系统所提供的本地方法,调用系统级的接口。(dll库是windows平台,而so库是Linux平台)
2、对于一些以前用C/C++实现过的库,可以用JNI来进行调用,而不用重新在Java层实现一次,节省时间。
3、对于某些特殊场景,需要高效率地执行代码,比如图形渲染的过程,这时候显然使用C/C++能极大提高运行速度。

JNI的缺点
使用了JNI与本地方法交互之后,牺牲了Java的可移植性这一优点,因为windwos和linux是不同的形式的库,必须在具体平台下重新编译。

笔者的环境平台
在开始之前,先说一下笔者所使用的IDE以及平台
1、Windows 10操作系统
2、IntelliJ IDEA Community Edition 2018.3.5 x64
3、Visual Studio 2017
4、JDK10

从Hello World开始

好了,说了一大堆,那么就让我们开始学习JNI,先从最简单的Hello World开始,先让代码跑起来,然后再继续深化学习。

1、在Intellij新建一个java项目

Java项目结构

①NativeTest类,里面仅声明了一个Native方法:


NativeTest类.png

native关键字表示这个方法由native层来实现,我们在java层不需要关注它的实现。

②Test类,这里声明了main方法,并调用NativeTest的native方法。


Test类.png

2、为native方法所在的类生成相应的头文件
上面的native方法在NativeTest类中,我们需要为此生成一个.h文件,只有这样我们才能把.h文件的方法用C/C++代码去实现。那么,怎么生成一个.h文件呢?JDK为我们提供了工具专门用于生成JNI所用的头文件。
格式是:javac -h
表示生成的.h文件所放置的位置。
表示待编译的源文件。
上面的命令需要在命令行中使用,我们可以用Intellij提供的Terminal去输入,也可以使用windows的cmd去输入,二者的效果是一样的。为了方便起见,笔者这里使用的是Terminal。

2.1、首先,我们在Terminal定位到NativeTest类所在的文件夹,即:


命令行1.png

2.2、然后,通过使用javac -h命令,编译NativeTest类,并生成与这个类有关的.h文件:


命令行2.png

注意:这里的使用了".",表示在当前目录直接生成.h文件。这里填入NativeTest.java,因为当前目录下有NativeTest.java文件,所以不需要添加额外的路径了。
2.3、运行之后,会发现当前目录下多了两个文件:NativeTest.class和com_jni_NativeTest.h文件:
项目目录结构2.png

所以我们知道,javac -h实际上做了两部操作,对NativeTest.java进行编译生成class文件,然后再生成.h文件。

3、新建一个windows桌面程序项目
这一步,在VS 2017中进行,新建一个项目如下图所示:

VS项目创建.png

我们这里选择动态链接库(DLL),因为我们需要这个dll库供java层使用,这里项目的位置可以保存在任何一个地方。

3.1、项目创建完毕之后,我们把刚才生成的com_jni_NativeTest.h文件复制到当前项目的文件目录下,如下图:


复制文件的路径.png

然后在VS2017的“解决方法资源管理器”中,在“头文件选项”,右键选择添加已有项,选中当前目录下刚才复制进来的com_jni_NativeTest.h文件:


添加现有项.png

3.2、接下来,我们需要把一些额外的文件,打开jdk的安装目录(笔者jdk的安装目录为:C:\Program Files\Java\jdk-10),在include文件夹下,复制jni.h和include\win32内的jni_md.h 这两个文件到NativeCode项目,即刚才存放com_jni_NativeTest.h文件的目录,同时把这两个文件作为现有项添加到头文件,步骤3.1的最后。
完成3.1和3.2之后,我们会看到有如下的头文件结构:


头文件目录结构.png

3.3、打开项目中的com_jni_NativeTest.h文件,把顶部的#include改成#include"jni.h",这样就不会报错了。解释一下为什么要做这样的改动:#include<>形式表示C/C++文件编译时,首先从编译器的类库路径里面寻找该头文件;而#include""表示在当前文件目录下寻找头文件。

3.4、关于com_jni_NativeTest.h文件的补充说明。
我们打开这个头文件,观察其内部结构:

头文件内部结构.png

我们可以看到,该头文件声明了一个方法Java_com_jni_NativeTest_sayHello(JNIEnv,jobject),该方法有两个关键字分别为JNIEXPORT和JNICALL 表示这个方法是要从Java层被调用的。然后该函数有两个形参:JNIEnv** 和 jobject。这两个参数是native方法自带的参数。
JNIEnv *是一个函数指针,它指向一系列JNI提供的函数来进行数据操作。
jobject表示调用这个函数的对象,因为在java层它是一个实例方法,所以实际上这个参数的作用类似于 this关键字

4、新建一个c++文件,实现.h文件所声明的函数
这里笔者直接使用VS帮我们生成的NativeCode.cpp文件进行操作:

C++文件实现.png

这里仅简单输出了hello world。

5、编译生成DLL文件
这里要使用x64的Debug调试器(默认是x86),点击上方的生成——>生成解决方案,可以观察到控制台输出了信息,并标明了生成的dll文件所在的位置,我们前往该位置,一般在 /项目目录/x64/debug/NativeCode.dll。我们复制这个库文件到Java项目内。在这里,笔者在java项目跟目录下,新建了一个native_libs的文件夹,把dll文件复制到这里:

Java项目结构2.png

6、加载DLL文件,并运行Main函数
DLL文件已经被添加到我们的Java项目了,接下来的操作就是要在JVM运行时加载它,以便我们后续调用native方法。我们在Test.java文件作点修改:

Test类2.png

好了,到目前为之,所有的工作都已经做完,让我们运行一下Main函数,看一下效果如何?


运行结果.png

如果你看到了上面的输出,那么恭喜你,你已经掌握了JNI调用的基本方法!

小结

上面详述了实现一个简单JNI调用的步骤,现在小结一下整个流程。
(1)在一个Java类中声明native方法。
(2)利用javac -h命令以该类为源文件生成一个.h文件
(3)在C/C++文件中实现该头文件所声明的方法
(4)编译C/C++文件,生成一个DLL或so文件,把它添加到java项目中
(5)通过System.loadLibrary方法加载这个库文件

踩过的坑

下面谈谈笔者在学习过程中遇到的一些问题,避免各位读者再度踩坑。
1、关于javac -h和javah命令
笔者在刚学习JNI的知识时,在生成.h文件阶段,在网上查的教程都是利用javah命令来进行操作。然而在笔者的电脑总提示"javah不是内部命令",然而javac是可以正常运作的,这说明并不是环境变量出了问题,这就有可能是jdk安装目录下压根就没有javah.exe,经过查找,确实是没有这个文件,所以javah命令会运行失败。
那么问题来了,为什么我的JDK没有javah.exe呢?
经过查阅资料,原来在JDK10以上的JDK内部已经去除了javah命令,它的功能被整合进了javac -h命令内。然而在jdk8以下的jdk是有该命令的。

解决途径:如果电脑安装的是jdk10以及10以上,使用javac -h命令;而jdk8以及8以下的使用javah -jni命令。

2、javac命令的进一步说明
javac命令是编译命令,将java源文件编译成class字节码文件。我们可以在命令行输入javac -help,了解它的使用方法:

javac.png

其基本语法为:javac ,其中可以是多个,而且也可以是多个,这时候表示把多个源文件同时编译。所以生成.h文件的命令格式为:javac -h
需要注意的是:如果少了选项(如果是当前目录直接用" . "代替),就会报错,笔者曾在这里卡了很长时间。

这篇文章到这里就结束了,希望对各位同学有所裨益:) 谢谢看到这里的你。下一篇文章将会围绕JNI的数据操作、函数操作部分进行详细讲解。

你可能感兴趣的:(JNI 入门(一):从Hello World开始)