标签归档:Java Native Interface

漫谈JNI

引子

我们首先来看一枚业界新闻:

北京时间4月6日,据国外媒体报道,TIOBE网站发布了最新的《四月份编程语言排名》,下面让我们一睹为快:

时隔4年之后,C语言荣登本月榜单中位居榜首,近十年来C语言的使用率一直稳定在15%-20%之间。这一次由于长期霸占第一宝座的Java略显颓势,从而给了C语言超过的机会。

随着JVM(Java虚拟机)可以支持越来越多的语言,Java的强大势力正在逐渐被瓜分。以JavaFX脚本为例,该语言的排名已经迅速蹿升至22位。

此外,Objective-C和谷歌Go语言依然是上升势头最明显的编程语言,显示出新语言的旺盛生命力。

TIOBE开发语言排行榜每月更新一次,依据的指数是基于世界范围内的资深软件工程师和第三方供应商,包括谷歌、微软等巨头公司均参与统计,其结果作为当前业内程序开发语言的流行使用程度的有效指标。

我们看到了Java语言生命力的旺盛,特别是在Java
2之后的版本,也就是JDK5(Tiger)发布之后,Java语言的应用更是爆炸式的增长,时值互联网飞速成长迅猛发展之际,Java语言与生俱来的跨平台特性以及良好且可控的安全性成为了互联网技术企业首选的开发语言。借着Sun公司当年在服务器市场上强劲的表现,Java语言顿时成为TIOBE排行榜上的新贵。

Java应用如此成熟与广泛,在Google.com上搜索Java得到的结果条数是“Results 1 – 10 of about 196,000,000 for  Java”,搜索C++得到的结果条数是“ Results 1 – 10 of about 53,300,000 for C++
”,那么我们再来看看关于JNI的搜索结果“ Results 1 – 10 of about 1,980,000 for Java Native Interface
”和Java 2D做一个对比“ Results 1 – 10 of about 2,710,000 for Java 2D”。从这些结果中我们能看出一些东西,Java
Native Interface(JNI)相较于Java中并不常用的Java 2D API的关注程度还低,也就是说JNI其实跟Java主体上的流行半毛钱关系都没有,那么JNI为什么要存在呢,JNI又能给我们带来什么呢?

JNI带来了什么

Java语言的成功很大程度上得于API的丰富和设计的良好,Java API为广大开发人员节省了大量的时间提高了开发效率,使得项目的交付更为敏捷。但是API注定存在其局限性,那么我们在什么时候需要使用到JNI呢?

1.         所开发的应用程序要使用到与平台相关的属性,而Java 标准类库不支持对这些属性的处理;

2.         已经拥有了用其他编程语言实现的应用程序或库,希望用Java 直接调用这些实现;

3.         程序的某个模块对运行的时间效率要求很高,从而希望用较低级的语言(如汇编)来实现,同时希望在Java 应用程序中使用这个模块。

从目前市场上对JNI应用的案例来看,最为主要的应用场景一般是源于上面的第二点原因,在已有类库的基础上进行开发平台的迁移。近来比较热门的Android移动终端OS平台就是基于JNI技术来实现的,本质上Android的核是Linux内核的子集,基于该内核使用Java平台进行了良好的封装,提供给开发人员一个高可用性的SDK
Platform。开发人员只需了解Java平台的特性,根据API
Documentation就完全可以开始编码了,并不需要知道其内部的实现机制,着实是为开发人员谋福利啊,当然此举更是从根本上劫持了诸多原本不属于该开发阵营中的开发人员,任何熟悉Java平台开发的开发人员顿时全成为了Android平台开发的潜在用户。技术也是可以带来市场的。

这就是JNI给我们带来的,借助Java语言的跨平台特性,以及开发阵营的强大,各大企业和机构基于已有的类库和程序进行二次封装,不仅能延续产品的持续发展,还能扩大二次开发人员的阵营,而不落于IT技术发展的潮流。说了这么多废话,那么如何使用JNI技术来进行二次封装呢?

Hello JNI World

1.         编写并编译Java代码

使用称手的工具(IDE)编写Java代码,例如保存在” E:\JavaWorkspace\Laputa\com\laputa\jni\test\HelloJNIWorld.java”

package com.laputa.jni.test

public class HelloJNIWorld{

public native void sayHello();

public static void main(String[] args){

HelloJNIWorld hello = new HelloJNIWorld();

hello.sayHello();

}

}

进入” E:\JavaWorkspace\Laputa\”,使用命令” javac com\laputa\jni\test\HelloJNIWorld.java”编译该文件,由于未指定编译结果路径,生成的class文件将保存在源文件同一目录下,得到”
e:\JavaWorkspace\Laputa\com\laputa\jni\test\HelloJNIWorld.class”。

2.         生成JNI头文件

使用javah命令生成头文件,”javah –jni com.laputa.jni.test.HelloWorld”,将会生成一个”
e:\JavaWorkspace\Laputa\com_laputa_jni_test_HelloJNIWorld.h”。在生成头文件的时候有两点需要注意,一是”javah –jni”
后面需要写类的全限定名(自行查阅资料了解全限定名相关知识),并且执行该命令的当前目录下要能找到全限定名中的路径,也就是说执行命令的路径应该在包路径的上一层目录,这里我们将com.laputa.jni.test包保存在”E:\JavaWorkspace\Laputa”下,那么我们执行javah命令的路径也应该是”
E:\JavaWorkspace\Laputa”;二是未指定生成头文件的目标目录时,默认将头文件生成在当前目录下。头文件的样式如下:

/* DO NOT EDIT THIS FILE – it is machine generated */

#include <jni.h>

/* Header for class com_laputa_jni_test_HelloJNIWorld */

#ifndef _Included_com_laputa_jni_test_HelloJNIWorld

#define _Included_com_laputa_jni_test_HelloJNIWorld

#ifdef __cplusplus

extern “C” {

#endif

/*

* Class:     com_laputa_jni_test_HelloJNIWorld

* Method:    sayHello

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_com_laputa_jni_test_HelloJNIWorld_sayHello

(JNIEnv *, jobject);

#ifdef __cplusplus

}

#endif

#endif

3.         实现头文件对应的方法

关于JNI头文件中的的类型以及相关的内容可以参考Java Tutorial中Java Native
Interface一章。现在我们就来实现刚才生成的头文件中的sayHello方法,至于方法名为何这么长,主要是因为JNI就是通过这样的机制来识别方法签名的,开发过程中需要注意的是,对于Java文件中的native方法最好不要使用重载,因为重载后的native方法生成的头文件中的方法将会非常之长,重载的方法名中将会包含其参数信息,例如在HelloJNIWorld.java中添加一个public
native void sayHello(String str);方法,其生成的头文件中对应方法将会是(下面标红的文字):

/*

* Class:     com_laputa_jni_test_HelloJNIWorld

* Method:    sayHello

* Signature: (Ljava/lang/String;)V

*/

JNIEXPORT void JNICALL Java_com_laputa_jni_test_HelloJNIWorld_sayHello__Ljava_lang_String_2

(JNIEnv *, jobject, jstring);

这会让我们在查看代码的时候迷惑,所以建议在需要重载的时候直接使用其他的名称,放弃重载为后期维护提供可读性更高的代码。

接下来就可以开始实现我们需要的方法了,例如我的Java代码中需要调用sayHello这个本地方法,那么我们新建一个对应的cpp文件,将方法签名拷贝至cpp文件中,实现该方法。

#include “com_laputa_jni_test_HelloJNIWorld.h”

#include <iostream>

using namespace
std;

/*

* Class:     com_laputa_jni_test_HelloJNIWorld

* Method:    sayHello

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_com_laputa_jni_test_HelloJNIWorld_sayHello

(
JNIEnv *, jobject)

{


std::cout << “Hello JNI World” << std::endl;

}

4.         编译链接库并添加至系统Path变量中

使用合适的编译器(VC/GCC)进行编译和链接,生成合适的链接库(dll或者so文件)。在编译过程中可能出现头文件包含出现错误的问题,主要是因为在生成的JNI头文件中有”#include
<jni.h>”语句,编译器会到系统路径下去查找外部头文件,需要我们在编译的IDE或者脚本中指定该文件所在的目录为编译时Include路径,另外”jni.h”文件中可能需要使用到与平台相关的一些文件,例如在Windows平台下需要使用到”jni_md.h”文件,该文件处于JDK安装目录的include目录下的win32路径下(其他平台也处于include的相应目录下),需要将这个文件所在目录也添加到编译时Include路径中,保证编译通过。将该文件加入系统Path目录下(不同的平台使用其特定方式进行添加,在非Windows平台下使用export命令设置名为LD_LIBRARY_PATH的路径参数值为库文件所在目录),以便Java程序能加载该库文件(Java加载库文件时,需要库文件在系统的Path下)。

5.         在Java程序中加载库文件,执行代码

在Java程序中使用System.loadLibrary(String libName)加载系统路径下的库文件(忽略后缀名)。加载库文件的代码可以使用静态域进行加载,使得类在加载之初就加载库文件,并且之后在当前JVM实例中共享使用,不需重复加载库文件。在代码中添加一段代码:

static{

System.loadLibrary(“HelloJNIWorld”);

}

重新编译,执行代码,程序将在控制台输出“Hello JNI World”

以上就是HelloWorld的整个编码过程了,流程如下:

  1. 编写Java代码,声明本地方法|
  2. 生成JNI头文件|
  3. 根据头文件编写JNI实现代码
  4. 编译工程,生成链接库
  5. 执行程序,加载链接库,查看结果

这就是JNI编码和调用的整个流程和步骤了,以上是一个非常简单的例程,实际的产品设计中会有更多的一些技巧和设计原则在里面。