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 <directory> <source file>
<directory>表示生成的.h文件所放置的位置。
<source file>表示待编译的源文件。
上面的命令需要在命令行中使用,我们可以用Intellij提供的Terminal去输入,也可以使用windows的cmd去输入,二者的效果是一样的。为了方便起见,笔者这里使用的是Terminal。

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


命令行1.png

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


命令行2.png

注意:这里的<directory>使用了".",表示在当前目录直接生成.h文件。<source file>这里填入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<jni.h>改成#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 <options> <source files>,其中<options>可以是多个,而且<source files>也可以是多个,这时候表示把多个源文件同时编译。所以生成.h文件的命令格式为:javac -h <directory> <source files>
需要注意的是:如果少了<directory>选项(如果是当前目录直接用" . "代替),就会报错,笔者曾在这里卡了很长时间。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容