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博客

相关推荐
吾日三省吾码1 小时前
JVM 性能调优
java
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh2 小时前
uiautomator案例
android
弗拉唐2 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi773 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
工业甲酰苯胺3 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3433 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀3 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20204 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea