Hello,I'm Shendi;
这次学习总结一下 JNI,包括制作的一些小demo(获取屏幕像素值,全局监听键盘事件).
目录
然后,我们需要获取到此java类对应的头文件(为我们c文件所使用)
我提供百度云下载方式(64位在线安装非常慢,压缩包形式又不知道是个啥)
什么是JNI?
JNI(Java Native Interface) Java本地接口,用于与C/C++进行交互
为什么要jni?
因为有些东西需要依赖于操作系统,
例如我上面说的 获取屏幕像素点,使用Java很难实现(可以截图然后获取图片的像素点)
以及键盘监听,Java的话只能在窗体上JFrame这些才可以获取到按下的上面键,但是用C可以很容易获取到
如果我们有这种需求,就需要使用到 JNI 了.
JDK中的很多实现都用到了 JNI,比如有时候点源码看到的 native 关键字...
除此之外,jni可以提高我们程序的运行性能,因为丢给底层去执行了,比如说nio,那么快的原因就是因为把某个操作丢给底层去执行...
用JNI实现Hello World
首先,编写Java类
我创建一个Java类,命名为 TestJNI,里面有一个native方法,然后main函数调用
我们的 hello 方法是无返回值 无参数的,先体验一下jni,我们要达到的效果就是让hello方法输出hello,world
main方法里调用了hello方法.
这个时候运行会出错
讲一下 native 关键字.
被 native 关键字修饰的方法代表本地方法,也就是实际操作是c/c++执行的,方法没有方法体.
至于方法的修饰(返回值,参数什么的)和以前使用的一样
然后,我们需要获取到此java类对应的头文件(为我们c文件所使用)
这里就是将我们java类的本地方法提取到头文件里.具体操作如下
- 注: 网上有很多教程都是使用 javah,但是在 java9以后,javah已经没了.
- 但是我们可以用 javac 来生成对应头文件,(java8的也有)
命令如下
javac -h <directory> 指定放置生成的本机标头文件的位置
我的Java文件为TestJNI.java
所以,javac命令为 javac TestJNI.java -h .
-h后面代表头文件生成的路径 . 是当前路径
然后看文件夹内就多了两个文件. TestJNI.class 和 TestJNI.h
TestJNI.h 是什么?
学过 c 的就知道, .h后缀的是c语言的头文件,我们用javac(或者javah)生成的这个头文件有什么意义?我们先看下内容
观察一下内容,第一行注释的意思为,不要修改这个文件...
然后其余的咱不管,主要看有注释的,发现有个地方和我们写的Java代码很相似的地方没?
- JNIExport(里面是大写),Export是导出的意思,就是一个标志
- void无返回值(我们java代码的方法也是无返回值)
- JNICall,Call是调用的意思,很容易理解
- Java_TestJNI_hello(...),对应于Java类的方法名(有包名的话会很长)
- JNIEnv *参数是一个指针,这个等于JNI对象,封装了很多函数,要想使用就需要此对象
- jobject 代表调用这个方法的对象,静态的则为此类.(与Java的方法传递this差不多)
看不明白?我们修改一下java代码,重复上面的步骤,新增一个方法 world,同样无返回值无参数
编译,这个时候的 .h 文件里的内容如下
可以很明显的看出多了一段,也就是这段代码对应我们java里的被native修饰的方法...
至于括号里的参数(换了个行别就看不懂了),这两个参数是固定参数,我们多一个参数的话就会在后面加...(可以自己试一下)
那么,我们该如何实现让 Java 的 native 方法调用c的方法呢?
接下来编写核心部分,TestJNI.c
注: 别看我 java文件和 h 文件 和 c文件命名一致,不一致也是没关系的
学过C的知道,上面的头文件是定义方法(在java里只有定义变量,没有定义方法的概念)
所以我们需要在c文件里实现对应方法,我们先需要包含进 TestJNI.h
使用 #include "TestJNI.h" 不用打分号,但是h文件和c文件要在同一个目录
然后我们要将 h 文件里的定义方法复制到c,并且从定义改成实现
world方法暂不实现,因为没被调用,我们在hello方法里输出hello,world
需要用到 stdio.h,(c的基础)
代码如下
编写好c代码后,我们需要生成为dll文件供java使用
安装MinGW
有c语言编译器的可以跳过这一步(gcc)
我这里也只提供windows版本的安装方法了(我的笔记)
在Windows上安装(注意32位和64位):
32位
需要先安装 MinGW,官网: https://osdn.net/projects/mingw/releases/
点击下面的windows菜单图标就可以下载了(next/continue就ok了),注意一下安装路径
装完后就会进入到图形化安装方式了,在Basic Setup中选择mingw32-gcc-g++-bin
然后点击Installation选择Apply Change进行安装
安装完后会在 MinGW 的bin目录下看到对应文件,将MinGW的bin目录配置到环境变量就ok了
64位
zip版: https://sourceforge.net/projects/mingw-w64/files/mingw-w64/mingw-w64-release/
下载exe版的: https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/installer/mingw-w64-install.exe
打开将 Architecture 设置,i686是32位,x86_64是64位
其余按需选择
需要特别注意软件位数,之前我安装的为32位的(看不出),然后java代码运行出现这个问题
然后就各种百度生成64位dll的方法...然后才知道我的gcc是32位的
我提供百度云下载方式(64位在线安装非常慢,压缩包形式又不知道是个啥)
链接:https://pan.baidu.com/s/1ipYvTlp_tGqssQD0CBo3Pw
提取码:2hyj
安装完后将 bin 目录 放到环境变量里(Java基础第一节应该就学了环境变量?) 方便找到gcc
使用 gcc 生成 dll
命令为 gcc -shared TestJNI.c
运行后会发现如下问题
也就是找不到 jni.h 的位置
jni.h 是什么?
我们打开 JDK 目录,在 include 下会发现有一个名为 jni.h 的文件
于是就理所当然的加参数指定此目录继续编译
使用 -I xxx来指定寻找头文件的目录
然后还需要一个 jni_md.h 的头文件
这个文件对应于操作系统,比如我的是windows,在include目录下右 win32 目录
里面就可以看到 jni_md.h 了
于是加上去编译
因为大意,复制过来的忘记加变量名了(C语言可以这样只定义方法的参数类型).
加上变量名
再次编译
发现文件夹下多了个这个,这不是我们想要的东西(.exe)
需要使用 -o命令来指定生成的文件名
有两种使用方式
- gcc -o xxx.exe xxx.c 这种是将xxx.c 编译为xxx.exe
- gcc xxx.c -o xxx.exe 这种是直接指定xxx.c生成为 xxx.exe(推荐用这种)
将a.exe删除,再次编译,命令为 gcc -shared TestJNI.c -I E:\jdk-14.0.1\include -I E:\jdk-14.0.1\include\win32 -o TestJNI.dll
dll文件就被整出来了
我们现在运行会发现还是出错,是因为我们没有加载dll.
加载dll文件
这里我们需要修改一下java代码,(因为我们本地方法的类和运行类是同一个)
使用 System.load("dll文件路径");
注: 这里的dll文件路径必须是绝对路径,不是的话运行会出错.
至于代码的位置肯定是在使用本地方法前
因为我们本地方法没变,所以我们只需要重新编译java文件就ok,也可以直接使用 java TestJNI.java 直接运行(会自动重新编译,.class文件不变的那种)
上面这种运行方式不会修改class文件,我们可以直接运行试一下
还是刚开始的那种问题(没有被重新编译).
至此,jni就已经学会基本使用了.
JNI对应类型处理
八大基本类型都有对应,不是八大基本类型则需要格外处理
八大类型对应
Java类型 | 本地类型 | bit |
boolean | jboolean | 8, unsigned |
byte | jbyte | 8 |
char | jchar | 16, unsigned |
short | jshort | 16 |
int | jint | 32 |
long | jlong | 64 |
float | jfloat | 32 |
double | jdouble | 64 |
以上类型的数组也可以转换 | 转换后的为jxxxArray |
引用数据类型
Java的类型 | JNI的引用类型 | 类型描述 |
java.lang.Object | jobject | 可以表示任何Java的对象,或者没有 |
java.lang.String | jstring | 字符串对象 |
java.lang.Class | jclass | Class类型对象 |
java.lang.Throwable | jthrowable | 表示异常 |
数据类型描述符
在 JVM 中,存储数据类型名称是用指定描述符来存储
Java类型 | 类型描述符 |
int | I |
long | J |
byte | B |
short | S |
char | C |
float | F |
double | D |
boolean | Z |
void | V |
其他引用类型 | L全名; (注意后面的分号) |
数组 | [ |
方法 | (参数)返回值 |
- 例如引用类型 String
- Java类型 java.lang.String
- JNI描述符 Ljava/lang/String;
- 数组
- Java类型 String[]
- JNI描述符 [Ljava/lang/String;
- Java类型 double[]
- JNI描述符 [D
- 方法((参数)返回值)
- Java方法 void test();
- JNI描述符 ()V
- Java方法 int[] test(String str,int num,double money);
- JNI描述符 (Ljava/lang/String;ID)[I
- 如果位于一个嵌入类,使用$作为类名间的分隔符
自定义类型转换
有些没有对应的则需要自行转换
首先我们需要使用到 JNIEnv,对应API可以从此获取: https://download.csdn.net/download/qq_41806966/12581285
主要用到五个方法
- FindClass 查找类
- 补充:在编写本地方法时,包名中的点(.)应改为斜杠(/),例如Object类应填入 java/lang/Object 而不是 java.lang.Object
- GetMethodID 获取方法id
- NewObject 创建对象
- GetFieldID 获取字段id
- SetIntField 设置字段id,这里如果是double类型就将中间的Int改成Double
先测试一下,创建Java类 生成h,然后新建c文件进行实现...
在 C 文件内我们需要用到 JNIEnv,这个是指针类型的,使用方法为 (*env)->方法
至于好像也可以直接用 env->,反正这个我是不能用,不是jdk版本的问题(或许是位数问题?)
并且所有的 env 的方法第一个参数都是传递 env(网上有些没有此参数,被害惨了...)
我们需要实现获取鼠标位置并返回 TestJNIPoint 类
先创建类
获取到此类的构造函数
特别注意: 第四个参数为描述符,也就是上面所说的,正常的话像上面这样是没问题的,但是我们使用的是内部类
括号内代表参数,我们应该将此类的外面一层的类的对象传进来
(踩坑)看我下面就知道测试了多少次(还不止),程序一切正常,当运行的时候JVM就出错了(是JVM).
如果想上面那样写获取不到内部类构造函数,然后JVM出错,就多了一个像下面这样的文件
所以我们的代码为(TestJNIPoint是TestJNIObj的内部类)
接下来创建对象
我们返回的参数就是这个对象了
接着我们需要获取到鼠标当前位置
需要使用到 C 的函数(我用的windows,所以在 windows.h 文件里有)
以前的笔记,使用起来很简单,只要定义 POINT,然后传给方法,接下来就可以使用 POINT的x和y了
接下来我们只需要给对象赋值 然后返回就可以了,通过获取字段id,设置字段值
建议在使用的时候先判断空(NULL 大写),不然有空并执行了JVM就会出错(像我上面那样)
编译运行 看下结果
JNI原理
想了解原理的可自行百度,或者我这里提供了一个链接
https://blog.csdn.net/hackooo/article/details/48395765/
实现用Java获取屏幕像素点
接下来我们制作刚一开始讲的 Java 不能实现的功能.
要实现获取屏幕像素点在 c 里很简单(我的是Windows操作系统)
windows.h 头文件里有如下方法(针对于 windows,其他操作系统可以自行百度)
首先我们需要提供获取当前鼠标位置的方法和指定位置像素值的方法
所以我们有内部类Color来表示颜色,Point表示位置(静态,相当于直接外部类)
并且有以上基础,可以直接用java实现获取鼠标位置像素值的方法
Java代码如下
/**
* 获取屏幕像素点的工具类.
* @author Shendi <a href='tencent://AddContact/?fromId=45&fromSubId=1&subcmd=all&uin=1711680493'>QQ</a>
*/
public class GetPixel {
/**
* 获取鼠标位置的位置.
*/
public static native Point getMousePos();
/**
* 获取指定位置的像素值.
*/
public static native Color getPixel(Point point);
/**
* 获取鼠标位置像素值
*/
public static Color getMousePixel() {
return getPixel(getMousePos());
}
/**
* 代表一个颜色 RBG.
* @author Shendi <a href='tencent://AddContact/?fromId=45&fromSubId=1&subcmd=all&uin=1711680493'>QQ</a>
*/
public static class Color {
int r,g,b;
}
/**
* 代表一个二维坐标
* @author Shendi <a href='tencent://AddContact/?fromId=45&fromSubId=1&subcmd=all&uin=1711680493'>QQ</a>
*/
public static class Point {
int x,y;
}
}
接下来生成头文件和新建c文件,c文件内容如下
#include "GetPixel.h"
#include <stdio.h>
#include <windows.h>
// 静态方法,所以第二个参数不再是jobject,因为不需要对象
// 获取鼠标位置
JNIEXPORT jobject JNICALL Java_GetPixel_getMousePos(JNIEnv * env, jclass cls) {
// 需要 Point 类,创建对象然后获取鼠标位置返回
jclass point = (*env)->FindClass(env,"GetPixel$Point");
if (point == NULL) return NULL;
jmethodID method = (*env)->GetMethodID(env,point,"<init>","()V");
if (method == NULL) return NULL;
jobject obj = (*env)->NewObject(env,point,method);
if (obj == NULL) return NULL;
POINT p;
GetCursorPos(&p);
jfieldID x = (*env)->GetFieldID(env, point, "x", "I");
(*env)->SetIntField(env, obj, x, p.x);
jfieldID y = (*env)->GetFieldID(env, point, "y", "I");
(*env)->SetIntField(env, obj, y, p.y);
return obj;
}
// 获取屏幕指定像素点的像素值
// 有 SetIntField就有 GetIntField
JNIEXPORT jobject JNICALL Java_GetPixel_getPixel(JNIEnv * env, jclass cls, jobject obj) {
// 创建所需要的对象和获取到所需要的id
// Point的xy
jclass point = (*env)->GetObjectClass(env, obj);
if (point == NULL) return NULL;
jfieldID x = (*env)->GetFieldID(env, point, "x", "I"), y = (*env)->GetFieldID(env, point, "y", "I");
// Color对象和rgb
jclass color = (*env)->FindClass(env,"GetPixel$Color");
if (color == NULL) return NULL;
jmethodID colorMethod = (*env)->GetMethodID(env,color,"<init>","()V");
if (colorMethod == NULL) return NULL;
jobject colorObj = (*env)->NewObject(env,color,colorMethod);
if (colorObj == NULL) return NULL;
jfieldID r = (*env)->GetFieldID(env,color,"r","I"), g = (*env)->GetFieldID(env, color, "g", "I"), b = (*env)->GetFieldID(env, color, "b", "I");
// 获取屏幕像素点,三步,获取屏幕dc,通过dc获取像素点,关闭dc(不关闭会越来越卡)
HDC hdc = GetDC(NULL);
jint xValue = (*env)->GetIntField(env, obj, x);
jint yValue = (*env)->GetIntField(env, obj, y);
COLORREF cref = GetPixel(hdc, xValue, yValue);
ReleaseDC(NULL,hdc);
// 设置rgb值
(*env)->SetIntField(env, colorObj, r, GetRValue(cref));
(*env)->SetIntField(env, colorObj, g, GetGValue(cref));
(*env)->SetIntField(env, colorObj, b, GetBValue(cref));
return colorObj;
}
特别注意(踩坑记录):
用 gcc或者g++ 编译遇到如下问题
大致问题就是说找不到xxx,问题出在了 GetPixel 函数(可能是这个编译器没有?)
整了我老久,最后解决办法就是用vs进行编译(如果没问题可省略下面这一步)
方法如下
使用 vs 进行编译
请自行下载 vs,并安装对应包.
首先,新建一个空项目
然后,直接将c文件和h文件拖到对应位置(不是复制,相当于链接个位置)
然后我们需要设置一下属性,点击Project1,右键,属性
然后点击 C/C++,我们需要链接一下 jni.h (这样也挺方便的,不需要用命令 -I什么的了,最主要的是还有代码提示)
点击编辑,将jni.h的那个目录和jni_md.h的那个目录加进去
然后确定,会卡一下,接下来生成就ok了
然后就在对应路径获取到dll文件就可以使用了
编写Java类测试
将dll复制到指定目录,编写 Test.java 文件,内容如下
再一次踩坑: 有时候我们直接使用 java Test.java 来进行运行(自动编译的那种),这个时候因为我们加载dll在这个类,所以会出错,只能先编译,在运行
运行结果如下(输出鼠标位置的颜色,我把鼠标放到上面的红字上)
上面的颜色应该不是纯红...
实现全局键盘监听
接下来这个操作在 Java 中只能通过 jni 来进行获取(全局的键盘监听)
我们需要提供一个本地方法(指定键是否被按下的方法)
以及一个Java方法用于调用监听(按下了哪个键),这里比较麻烦,具体实现就是通过判断指定键是否按下,按下就调用指定的...
也可以使用键盘钩子(C代码),但是那个比较麻烦.
Java代码如下
/**
* 键盘工具类
* @author Shendi <a href='tencent://AddContact/?fromId=45&fromSubId=1&subcmd=all&uin=1711680493'>QQ</a>
*/
public class GetKey {
/**
* 当前的键盘监听
*/
private static KeyListener listener;
/**
* 判断键盘监听是否启动了
*/
private static boolean isStart;
/**
* 获取指定键是否被按下.
* @return <0则被按下,>0则没有被按下
*/
public static native int getKeyState(int key);
/**
* 设置当前的键盘监听.
*/
public static void setKeyListener(KeyListener listener) {
GetKey.listener = listener;
// 没有开启则线程内开启
if (!isStart) new Thread(() -> start()).start();
isStart = true;
}
/**
* 开启键盘监听
*/
private static void start() {
// 这里就处理英文26键,需要的可以自行加
char[] keys = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
// 记录被按下的按键,用于获取状态
ArrayList<String> pressKey = new ArrayList<>();
// 死循环执行,获取有无按键按下/抬起
while (true) {
for (char k : keys) {
int keyState = getKeyState((int)k);
if (pressKey.contains(String.valueOf(k))) {
if (keyState < 0) {
// 键盘已经按下并且还在按下
listener.keyPressed(k);
} else {
// 键盘已经按下 抬起
listener.keyReleased(k);
pressKey.remove(String.valueOf(k));
}
} else if (keyState < 0) {
// 键盘没有按下 按下
listener.keyTyped(k);
pressKey.add(String.valueOf(k));
}
}
}
}
/**
* 键盘监听,自己定义个,如果用java.awt的需要创建KeyEvent.
*/
public static interface KeyListener {
/**
* 键盘按下中
*/
void keyPressed(char key);
/**
* 键盘按下了
*/
void keyTyped(char key);
/**
* 键盘抬起了
*/
void keyReleased(char key);
}
}
上面监听按下了什么键的方法其实就是一个线程里跑死循环判断键是否被按下
接下来生成h文件,新建c文件
c文件代码如下(很简单,就获取键是否按下,返回)
更改一下生成的文件名,不再是 GetPixel了,因为我们有两个功能
最后,复制,测试
结果如下
因为我截图用的是QQ截图,Ctrl + A,以及这是全局键盘监听(在后台也可以跑)
结尾
写的demo还是有点用处的,所以这里我提供了这篇文章的所有文件(我的操作系统是Windows的)