概述
官方文档:Java Native Interface 6.0 Specification;
Java Native Interface (JNI) 标准是 Java 平台的一部分,它允许 Java 代码和其他语言写的代码进行交互。JNI 是本地编程接口,它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程语言(如 C、C++ 和汇编语言)编写的应用程序和库进行交互操作。
JNI 作用
- 扩展:JNI 扩展了 JVM 能力,驱动开发,例如开发一个 wifi 驱动,可以将手机设置为无限路由;
- 高效:本地代码效率高,游戏渲染,音频视频处理等方面使用 JNI 调用本地代码,C语言可以灵活操作内存;
- 复用:在文件压缩算法 7zip 开源代码库,机器视觉 OpenCV 开放算法库等方面可以复用 C 平台上的代码,不必在开发一套完整的 Java 体系,
避免重复发明轮子; - 特殊:产品的核心技术一般也采用 JNI 开发,不易破解。
一、基本流程
下面通过一个示例来了解 java 使用 jni 的基本流程。
环境如下:
Linux 4.13.0-16-generic;
gcc version 7.2.0;
openjdk version "1.8.0_151";
javac 1.8.0_151;
java 集成开发工具:IDEA 2017。
1.1、创建 native 方法
项目结构如下:
JNI示例项目结构.png首先在 java 中声明 native 方法,示例代码如下:
private static native void helloJni();
1.2、生成头文件
我使用的 java IDE 是 IDEA,只要在 Terminal 中输入以下指令就可以在对应的目录下生成相应的头文件,
javah -jni -classpath out/production/Jni_01 -d ./jni com.seraphzxz.Main
以上指令中 out/production/Jni_01 为目标文件所在目录,./jni 为输出目录,com.seraphzxz.Main 为目标文件,也就是声明了 native 方法的 java 类。生成的头文件名称为 com_seraphzxz_Main.h,代码如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_seraphzxz_Main */
#ifndef _Included_com_seraphzxz_Main
#define _Included_com_seraphzxz_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_seraphzxz_Main
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_seraphzxz_Main_sayHello
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
1.3、实现头文件
创建实现头文件的实现类 Main.c,代码如下:
#include <stdio.h>
#include "com_seraphzxz_Main.h"
JNIEXPORT void JNICALL Java_com_seraphzxz_Main_helloJni(JNIEnv *env, jobject thisObj) {
printf("Hello JNI.\n");
return;
}
实现方法很简单就是打印 Hello JNI.。
1.5、生成动态链接库
这里要注意的是,在编译 so 库文件时,需要把头文件中的 #include <jni.h> 改为 #include "jni.h",我这里把 jni.h 和 jni_md.h 都添加到了 jni 目录下,去掉了 Main.c 中的 #include <jni.h>。执行以下指令生成 .so 库:
gcc -shared -fpic -o libmain.so ./jni/Main.c
也可先编译为可重定位目标程序,也就是 .o 文件,指令如下:
gcc -c jni/Main.c
接着在转化为 .so,指令如下:
gcc -shared -o libmain.so Main.o
注意这里生成的 .so 库的命名方式 —— 添加 lib 前缀(约定)。
1.6、配置环境
因为系统的 JVM 的 java.library.path 属性即为环境变量 Path 指定的目录,但是 .so 并未放入到 Path 指定的任何一个目录中,因此需
要告诉 JVM,.so 文件所在的目录。在 IDEA 中点击 Run > Edit Configurations 并配置 MV Option:
-Djava.library.path=/.so所在目录
其中 -Djava.library.path 为固定写法,等号右面的就是 .so 库所在的目录。
不然会报以下错误:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no main in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at com.seraphzxz.Main.<clinit>(Main.java:7)
1.7、加载共享库
java 中加载 .so 共享库的代码如下:
static {
System.loadLibrary("main");
}
注意这里加载共享库的方法 loadLibrary() 中输入的参数为 "main",而我们生成的共享库名称为 libmain.so,这是个约定,要注意一下,不然会报以下错误:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no main in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at com.seraphzxz.Main.<clinit>(Main.java:7)
当然了,这只是报该错误的原因之一。
1.8、调用 native 方法
完整的 java 代码如下:
public class Main {
static {
System.loadLibrary("main");
}
private static native void helloJni();
public static void main(String[] args) {
helloJni();
}
}
执行结果:
Hello JNI.
到这里一个完整的 JNI 调用流程就走通了,下面就来分析 Java 的 native 方法是如何与 C/C++ 中的函数链接的。
二、JNI 的注册方式
2.1、静态注册
原理:根据函数名建立 Java 方法和 JNI 函数的一一对应关系。流程如下:
- 先编写 Java 的 native 方法;
- 用 javah 工具生成对应的头文件;
- 实现 JNI 里面的函数,在 Java 中通过 System.loadLibrary 加载 so 库。
静态注册的方式有两个重要的关键词:JNIEXPORT 和 JNICALL,这两个关键词是宏定义,主要是注明该函数是 JNI 函数,当虚拟机加载 so 库时,如果发现函数含有这两个宏定义时,就会链接到对应的 Java 层的 native 方法。
这里顺便说一下 JNI 函数命名规则,一个本地方法的函数名分为如下几个部分:
- Java_ 前缀;
- 以“_” 为分隔符的类名全称;
- “_”分隔符;
- 方法名;
对于重载方法(overload),后面还要跟两个下划线及参数签名(因为 Java 的方法签名除了方法名,还有参数,避免冲突所以重载方法需要加上后缀避免冲突)。
对于一些特殊字符,使用转义字符来代替,例如作为分隔符的下划线如果在方法名中,则会被替换成 _1,具体替换看下表:
转义字符 | 含义 |
---|---|
_0XXXX | Unicode 字符 |
_1 | 下划线 _ |
_2 | 分号 ; |
_3 | 中括号 [ |
使用静态连接的优点:
- 实现比较简单,可以通过 javah 工具将 Java代码的 native 方法直接转化为对应的 native 层代码的函数;
缺点:
- javah 生成的 native 层函数名较长,可读性很差;
- 后期修改文件名、类名或函数名时,头文件的函数将失效,需要重新生成或手动改;
- 程序运行效率低,首次调用 native 函数时,需要根据函数名在 JNI 层搜索对应的本地函数,建立对应关系,比较耗时。
2.2、动态注册
原理:直接告诉 native 方法其在 JNI 中对应函数的指针。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关
联关系,步骤如下:
- 编写 Java 的 native 方法;
- 编写 JNI 函数的实现(函数名可以随便命名);
- 利用结构体 JNINativeMethod 保存 Java native 方法和 JNI 函数的对应关系;
- 利用 registerNatives(JNIEnv* env) 注册类的所有本地方法;
- 在 JNI_OnLoad 方法中调用注册方法;
- 在 Java 中通过 System.loadLibrary 加载完 JNI 动态库之后,会调用 JNI_OnLoad 函数,完成动态注册。
通过下面的代码示例来分析 JNI 的动态注册方式。
直接看实现类:
#include "jni.h"
#include <stdio.h>
#include <stdlib.h>
using namespace std;
#ifdef __cplusplus
extern "C" {
#endif
static const char *className = "com/seraphzxz/Main";
static void helloJni(JNIEnv *env, jobject, jlong handle) {
printf("Hello JNI.");
}
static JNINativeMethod gJni_Methods_table[] = {
{"helloJni", "()V", (void*)helloJNi},
};
static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = (env)->FindClass( className);
if (clazz == NULL) {
return -1;
}
int result = 0;
if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
result = -1;
}
(env)->DeleteLocalRef(clazz);
return result;
}
// 重点看该函数
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
return JNI_VERSION_1_4;
}
#ifdef __cplusplus
}
#endif
在实际的应用中,可以将静态注册和动态注册结合起来:在 java 代码中仍然声明一个 native 函数,但是这个函数仅仅是用来去触发在 JNI 层的 native 函数的动态注册。看下面示例代码:
java 层:
static {
System.loadLibrary("jni");
registerNatives();
}
private static native void registerNatives();
JNI 层:
通过 javah 生成 java 层声明的 native 函数的文件,并且在实现代码中去动态注册 JNI 函数:
JNIEXPORT void JNICALL Java_com_seraphzxzi_NativeRgister_registerNatives
(JNIEnv *env, jclass clazz){
(env)->RegisterNatives(clazz, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
}
网友评论