参考链接: Java JNI实现原理初探 JAVA JNI简单实现 JAVA基础之理解JNI原理
介绍
在Android的Native项目中,我们在Java层定义JNI函数时,Android Studio会自动帮我们编译并创建JNI函数 及 自动生成SO库。本篇文章则是在Java项目中,手动使用javah生成头文件,手动调用gcc编译so库;进行了一个完整的JNI实现流程。
JNI的简单实现(静态注册+生成动态库)
假设当前的目录结构如下:
markdown
-
| - xunua
| Test.java
1、首先编写java文件:
java
package xunua;
public class Test{
static{
System.loadLibrary("bridge");
}
public native int nativeAdd(int x,int y);
public static void main(String[] args){
Test obj = new Test();
System.out.printf("%d\n",obj.nativeAdd(2012,3));
}
}
代码很简单,这里声明了 nativeAdd(int x,inty) 的方法,执行的时候简单的打出执行的结果。另外这里调用API去加载 名为 bridge 的库,接下来就来实现这个库。
2、生成JNI调用需要的头文件:
生成头文件需要使用到javah.exe工具,这个工具在Java SDK的bin目录中。
javac xunua/Test.java
javah -jni xunua.Test
第一行代码的作用是:编译xunua包下的Test.java 文件,生成Test.class可执行文件文件。
第二行代码的作用是:将xunua包下的Test类,通过javah生成JNI调用所需要的.h头文件:xunua.h
执行完javah命令后的目录结构是这样的:
markdown
-
| - xunua
| Test.java
| Test.class
| - xunua_Test.h
xunua_Text.h头文件内容如下:
arduino
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class xunua_Test */
//对_Included_xunua_Test的判断是为了避免头文件的重复引入。
#ifndef _Included_xunua_Test //如果宏_Included_xunua_Test没有被定义,那么执行下面的代码。
#define _Included_xunua_Test //定义宏_Included_xunua_Test。
#ifdef __cplusplus //判断当前编译器是否 是支持 cplusplus,如果是C++的编译器,那么就添加extern"C"让编译器以C的方式编译。 如果不是C++的环境,说明是C的环境,那么就不需要添加extern"C"了。
extern "C" {
#endif
JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
对javah工具生成.h头文件的代码阅读
经常会见到__cplusplus关键字,比如下面的代码:
arduino
#ifdef __cplusplus //当前是否是CPP文件中
extern "C" {
#endif
JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
这里面,两种关键字,都是为了实现C++与C兼容的;extern "C"是用来在C++程序中 声明或定义一个C语言代码块的符号,比如:
csharp
extern "C" {
int func(int);
int var;
}
上面的代码中,C++编译器会将 在extern "C"的大括号内部的代码当做C语言来处理。
由于C和C++毕竟是不同的,为了实现某个程序在C和C++中都是兼容的,如果定义两套头文件,未免太过麻烦,所以就有了cplusplus的出现,这个是在C++中特有的, cplusplus就是C++,也就有了上面第一段代码的使用。
- 如果这段代码是在C++文件中出现,那么经过编译后,该段代码就变成了:
markdown
/**********C++文件中条件编译后的结果***************/
extern "C" {
JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
(JNIEnv *, jobject, jint, jint);
}
- 而在C文件中,经过条件编译,该段代码变成了:
arduino
JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
(JNIEnv *, jobject, jint, jint);
3、native方法的实现
接下来新增bridge.c文件来实现之前声明的native方法,目录结构如下:
markdown
-
| - xunua
| Test.java
| Test.class
| - xunua_Test.h
| - bridge.c
bridge.c的内容如下:
arduino
#include "xunua_Test.h"//将javah生成的头文件引入
//对头文件声明的函数进行定义
JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
(JNIEnv * env, jobject obj, jint x, jint y){
return x+y;
}
这里的实现只是简单的把两个参数相加,然后返回结果。
4、生成动态链接库
配置好mingw64工具后,在bin目录下有gcc.exe工具。
linux指令:gcc -shared -I /usr/lib/jdk1.6.0_45/include-I /usr/lib/jdk1.6.0_45/include/linux bridge.c -o libbridge.so
windows指令:gcc -shared -o dll_demo.dll bridge.c //将bridge.c文件 编译成windows可执行文件dll_demo.dll。
注意:gcc编译时如果出现找不到jni.h库,那么就去sdk中将jni.h和jni_md.h导入过来当前目录作为本地文件。检查bridge.c的引入方式,include<>是引入系统库中文件,include" "是引入本地的文件;避免引入方式不正确而无法编译。
我们在java代码中调用的是**System.loadLibrary**("xxx")
加载so库,那么生成的动态链接库的名称就必须是libxxx.so
的形式(这里指Linux环境;windows中生成是.dll后缀(即:dll_xxx.dll形式),使用'System.load ("xxx.dll")'进行加载,它们在使用上没有区别),否则在执行java代码的时候,就会报 java.lang.UnsatisfiedLinkError: no XXX in java.library.path
的错误!也就是说找不到这个库。
生成动态链接库之后的目录结构如下:
markdown
-
| - xunua
| Test.java
| Test.class
| - xunua_Test.h
| - bridge.c
| - libbridge.so
5、执行代码验证结果
java -Djava.library.path=. xunua.Test
//输出2015
Java调用JNI的最简单例子完成。
此处为语雀内容卡片,点击链接查看:www.yuque.com/u26760130/x...
JNI技术实现原理
我们知道cpu只认得 "0101101" 类似这种二进制符号, C、C++ 这些代码最终都得通过编译、汇编成二进制代码,cpu才能识别并执行。。而Java比C、C++又多了一层虚拟机,过程也复杂许多。Java代码经过编译成class文件、虚拟机装载等步骤最终在虚拟机中执行。class文件里面就是一个结构复杂的表,而最终告诉虚拟机怎么执行的就是靠里面的字节码说明。
Java虚拟机在执行的时候,可以采用解释执行和编译执行的方式执行,但最终都是转化为机器码执行。
Java虚拟机运行时的数据区,包括:方法区、虚拟机栈、堆、程序计数器、本地方法栈。
问题来了,按目前的理解,如果是解释执行,那么方法区中应该存的是字节码,那执行的时候,通过JNI 动态装载的c、c++库,放哪去?怎么执行?
那么,刚刚生成的动态链接库"libbridge.so"是如何装进内存的?native方法怎么调用?跟普通的方法调用有什么区别吗?
我们把Test.java改改,增加普通的方法 int add(int x,int y) :
arduino
public class Test{
static{
System.loadLibrary("bridge");
}
public native int nativeAdd(int x,int y);
public int add(int x,int y){
return x+y;
}
public static void main(String[] args){
Test obj = new Test();
System.out.printf("%d\n",obj.nativeAdd(2012,3));
System.out.printf("%d\n",obj.add(2012,3));
}
}
接下来将它编译成class文件,看看class文件中,native方法和普通方法有何区别:
javac hackooo/Test.java
javap -verbose hackooo.Test
解析后,"nativeAdd"和"add"两个方法的结果如下:
arduino
public native int nativeAdd(int, int);
flags: ACC_PUBLIC, ACC_NATIVE
public int add(int, int);
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 8: 0
可见,普通的"add"方法是直接把字节码放到code属性表中,而native方法,与普通的方法通过一个标志"ACC_NATIVE"区分开来。java在执行普通的方法调用的时候,可以通过找方法表,再找到相应的code属性表,最终解释执行代码,那么,对于native方法,在class文件中,并没有体现native代码在哪里,只有一个"ACC_NATIVE"的标识,那么在执行的时候改怎么找到动态链接库的代码呢?
深入原理参考此文章:Java JNI实现原理初探_jnibridge-CSDN博客