一文搞懂 Java 中的 Native 方法

1. 简介

1.1 个人理解

初次遇见 native 是在 java.lang.String 源码中的一个 intern 方法:

public native String intern();

因为还是第一次遇到,所以就去搜了一些文章进行了解。下面就对一些 Native 关键字进行一些总结。

native 也即 JNI —— Java Native Interface(Java 本地接口)。凡是一种语言,都希望是纯的。比如解决某一个方案就单单使用同一个语言来实现。而 Java 却不然,Java 平台有个用户和本地 C 代码进行相互操作的 API,称为 Java Native Interface (Java 本地接口)。也就是说,相当于使用 Java 语言声明了一个方法,而这个方法的具体实现是在其他语言(如 C、C++等)中实现的,所以 Java 中编写的也就类似于一个接口,只是这个接口被称作本地接口。

Java 使用本地接口也是有原因的,因为 Java 的平台无关性,有优势当然也有牺牲,它的缺点就是不能使用 Java 代码直接对一些底层进行操作,但是对底层的操作又是一个语言必不可少的,于是 Java 就想到了间接去操作底层,而中间利用的就是操作系统。所以有些方法,Java 声明为了 native ,具体的实现是在 DLL 中,JVM 去进行真正的操作。

简单记忆:native 方法是 Java 中声明,由操作系统中具体方法实现。

1.2 其他介绍

网友见解:

  • native 是与 C++ 联合开发的时候用的!java 自己开发不用的!
  • 使用 native 关键字说明这个方法是原生函数,也就是这个方法是用 C/C++ 语言实现的,并且被编译成了 DLL,由 Java去调用。
    这些函数的实现体在 DLL 中,JDK 的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的。这也是 Java 的底层机制,实际上 Java 就是在不同的平台上调用不同的 native 方法实现对操作系统的访问的。
  • native 是用做 java 和其他语言(如c++)进行协作时用的也就是 native 后的函数的实现不是用 Java 写的,既然都不是 Java,那就别管它的源代码了,呵呵。
  • native 的意思就是通知操作系统,这个函数你必须给我实现,因为我要使用。所以 native 关键字的函数都是操作系统实现的,Java 只能调用。
  • Java 是跨平台的语言,既然是跨了平台,所付出的代价就是牺牲一些对底层的控制,而 Java 要实现对底层的控制,就需要一些其他语言的帮助,这个就是 native 的作用了
  • Java 不是完美的,Java 的不足除了体现在运行速度上要比传统的 C++ 慢许多之外,Java 无法直接访问到操作系统底层(如系统硬件等),为此 Java 使用 native 方法来扩展 Java 程序的功能。

Java 本地方法适用的情况:

  1. 为了使用底层的主机平台的某个特性,而这个特性不能通过 Java API 访问;
  2. 为了访问一个老的系统或者使用一个已有的库,而这个系统或这个库不是用 Java 编写的;
  3. 为了加快程序的性能,而将一段时间敏感的代码作为本地方法实现。

2. 用 Java 调用 C 的实例

为了更好的理解 Java 中调用 Native 方法,特来编写一个具体的小的测试。

以下所有文件都存于个人本地文件夹:C:\Users\Eric\Desktop\NativeTest。

2.1 创建包含本地方法的类

在文件夹下创建一个 HelloNative.java 文件,里面包含着一个 native 的方法和加载库的方法 loadLibrary。代码如下:

public class HelloNative {
    
    
    static {
    
    
        // 注意加载库的名字为 HelloNative,需要与下文的生成文件保持一致
        System.loadLibrary("HelloNative");
    }
     
    public static native void sayHello();
     
    @SuppressWarnings("static-access")
    public static void main(String[] args) {
    
    
        new HelloNative().sayHello();
    }
}

首先注意的是 native 方法,然后那个加载库的静态代码块在后面也起作用。native 关键字告诉编译器(其实是 JVM)调用的是该方法在外部定义,这里指的是 C。

2.2 编译运行

在当前文件夹下使用 CMD 命令行编译 HelloNative.java,如下。

编译生成字节码文件

如果当前类中没有 Native 方法,那么我们可以直接使用 java 命令直接运行,但是此时大家直接运行这个代码,会出现以下结果:

运行出错

意思是虚拟机说不知道如何找到 sayHello。因为我们定义的 sayHello 方法为 native 类型,所以我们还需要再进行下文的操作步骤。

2.3 获得头文件

在当前文件目录下运行 javah,得到包含该方法的 C 声明头文件 。命令如下:

javah HelloNative       # 生成 .h文件

得到的结果如下:

生成头文件

得到的 HelloNative.h 文件,内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloNative */
 
#ifndef _Included_HelloNative
#define _Included_HelloNative
#ifdef __cplusplus
extern "C" {
    
    
#endif
/*
 * Class:     HelloNative
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloNative_sayHello
  (JNIEnv *, jclass);
 
#ifdef __cplusplus
}
#endif
#endif

这个头文件中可以看见我们声明的 Java 本地化 sayHello 方法,对应 C 的声明:JNIEXPORT void JNICALL Java_HelloNative_sayHello(JNIEnv *, jclass);,我们只要实现这个方法即可。

注意:头文件中 jni.h 这个文件,是在本地 JDK 目录下的 include 文件夹中,例如我的目录:

jni.h路径

2.3 C 实现头文件的声明方法

生成了头文件之后,我们再在当前文件夹下创建一个 HelloNative.c 文件,并简单地实现 HelloNative.h 文件中声明的 sayHello 方法,代码如下:

// 包含刚才生成的.h文件
#include "HelloNative.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_HelloNative_sayHello(JNIEnv *env, jclass thisClass) {
    
    
    printf("Hello, Native!!");
}

结果:

创建C文件实现native方法

2.4 生成动态链接库

到了这一步,我们需要将上述两个文件 HelloNative.cHelloNative.h 编译为动态链接库。

这里说明两种方法:分步编译或一次性编译形成动态链接库文件。

(1)一次性编译

在 Windows CMD 命令行里,使用如下命令:

gcc -m64  -Wl,--add-stdcall-alias -I"C:/Program Files/Java/jdk1.8.0_181/include" -I"C:/Program Files/Java/jdk1.8.0_181/include/win32" -shared -o HelloNative.dll HelloNative.c

注意:上述 JDK 为个人本地路径,需要根据个人情况进行修改。-m64 表示生成 dll 库是 64 位的,参数 -I 指定头文件路径上述命令运行后,我们会在目录文件夹下生成相应的动态链接库文件,如下:

一次性编译生成动态链接库

如果使用的 Windows 上面没有 gcc,需要先下载压缩包然后配置一下环境变量即可使用(两分钟就搞定),压缩包及配置步骤,这里推荐一条博文:解决mingw-w64外网下载太慢问题,离线包安装配置过程讲解

(2)分步编译

为了演示分步编译,我们先把上面一次性编译生成的动态链接库文件 HelloNative.dll 给删除掉。然后再执行如下命令:

$ gcc -c -I"C:/Program Files/Java/jdk1.8.0_181/include" -I"C:/Program Files/Java/jdk1.8.0_181/include/win32" HelloNative.c  HelloNative.h

结果如下:

分布编译第一步

在这里我们只需要先关注生成的 HelloNative.o 文件,然后我们执行第二步骤的命令,将该文件编译为 dll 文件:

$ gcc -Wl,--add-stdcall-alias -shared -o HelloNative.dll HelloNative.o

结果如下:

分步编译第二步

2.5 再次运行Java类

使用 2.4 中的任意一种方法生成动态链接库 HelloNative.dll 文件后,我们再次使用 java 命令运行 Java 类,结果如下:

image-20201112145157672

我们可以看到 Java 类已经可以成功运行了,并且我们也可以看出它运行的实际是我们使用 C 语言编写的实现方法,它作为本地方法 native 来被 Java 代码调用。

2.6 总结

可以将 native 方法比作 Java 程序同C程序的接口,其实现步骤:

  1. 在 Java 中声明 native 方法,然后编译成 .class 文件;
  2. 用 javah 产生一个 .h 文件;
  3. 写一个 .c 或 .cpp 文件实现 native 导出方法,其中需要包含第二步产生的 .h 文件(注意其中又包含了JDK带的jni.h文件);
  4. 将第三步的 .c 或 .cpp 文件编译成动态链接库文件;
  5. 在 Java 中用 System.loadLibrary() 方法加载第四步产生的动态链接库文件,这个 native 方法就可以在 Java 中被访问了。

JNI 调用 C 流程图

调用流程图(来源于网络)

参考文章

猜你喜欢

转载自blog.csdn.net/weixin_43653599/article/details/111592025