Java项目中手动实现JNI(不借助Android Studio)

参考链接: 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博客

相关推荐
武子康23 分钟前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘1 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意1 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
FF在路上2 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
皓木.3 小时前
Mybatis-Plus
java·开发语言
不良人天码星3 小时前
lombok插件不生效
java·开发语言·intellij-idea
守护者1703 小时前
JAVA学习-练习试用Java实现“使用Arrays.toString方法将数组转换为字符串并打印出来”
java·学习
源码哥_博纳软云3 小时前
JAVA同城服务场馆门店预约系统支持H5小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台