众所周知,Java是一门跨平台语言,用Java可以完成很多工作。但Java也不是万能的,有时我们可能会需要调用很多本地应用或库,有可能会需要其他程序来调用Java。这就涉及到一个Java与其他语言程序的交互问题。对于本地程序,我们一般都会用C或C++或汇编语言等来编写,然后再编译为基于本地系统和硬件的程序,所以需要有一种让JVM中的程序调用本地程序的技术,这个时候JNI就出现了。当然也会有其他方式来实现本机应用和Java程序间的通讯,但某些情况下确实会用到JNI的方式来实现Java和本地程序的互相调用。
JNI (Java Native Interface)是一个标准的编程接口,定义了一种方式来实现JVM中的Java代码和本地代码间的交互。
因为工作需要,需要实现Java和C++类库间的调用,所以才了解到JNI,涉及到了JNI中Java调用C++中的方法,以及C++中回调Java方法。研究了些日子,也学习了很多。所以特此记录。
JNI基础概念
- JavaVM、JNIEnv、Jobject
- JavaVM
提供调用接口的函数,可以用来获取JNIEnv,可以跨线程使用。
- JNIEnv
JNI Environment,用此类型的实例便可以调用JNI提供的函数。它提供了大多数的JNI函数,是一个线程内的本地变量,无法跨线程访问。如果不得不在其他线程引用JNIEnv,则可以使用JavaVM的GetEnv来获取JNIEnv。
- Jobject
Jobject是本地方法中访问的Java中的具体对象。
- Global and Local References
JNI把本地代码使用的对象引用分成了两种:局部引用和全局引用。
- 局部引用
局部对象引用在在本地方法的调用期间有效,一旦这个本地方法执行完成后,就会自动释放掉这个局部的对象引用。局部引用只在本线程中有效,跨线程无效。本地代码也不可以把本地引用从一个线程传递到另一个线程。
- 全局引用
全局对象引用会一直有效,除非显式的把这个引用释放掉。
JNI使用步骤
1、新建一个类。
声明native类型的方法,只声明;
声明需要回调的方法,进行实现。
2、编译Java源文件为class文件
3、用javah命令编译class文件为C++头文件
4、根据头文件编写C++源码文件
5、编译C++代码为动态库
* Windows:\*.dll
* linux/unix:\*.so
* os x:\*.jnilib
6、把动态库放到系统的对应目录下
本程序中,在Windows下,生成了dll文件,放到了JDK的bin中
7、加载动态库,运行Java程序
JNI调用C++
1、新建JNIDemo类,定义native方法和回调方法
|
|
2、把Java源文件编译为class文件
|
|
编译java文件的时候我加了一个参数encoding,因为我的电脑默认编码时GBK,而我用的Java文件编码为UTF-8,所以指定UTF-8编码来编译,避免编码不一致而出错的问题。
3、用javah命令把class文件编译为头文件
|
|
此命令把class文件编译为C++的头文件。
头文件JNIDemo.h中的内容如下
|
|
可以看出,头文件中已经自动导入了jni.h文件,我们在Java文件中声明的native方法也自动用C++语言声明了出来。而没有被native修饰的方法不会被识别。
生成的函数名明明规则为: Java_Java类名_Java中的方法名。
如果这个类是存在于一个包中,则生成的头文件中的方法名还会加入此Java类的包目录,因此,头文件中的方法声明规则如下:
|
|
此处的参数只是声明,并没有给出参数的标识,在自己编写源码文件时,应该都加上。
4、用对应的本地语言实现头文件中的函数
源码文件JNIDemo.cpp如下
|
|
5、生成动态库,放到对应目录下
本程序中,在Windows下,生成了dll文件,放到了JDK的bin中
6、加载动态库,调用Java类中对应的方法
|
|
7、运行
运行main方法,得到如下输出内容
注意
以上的例子是我在研究完工作内容后做的,工作工程中针对此部分还碰到了很多问题,有几个比较典型的,做一下记录。
1、class文件到头文件
在class的目中执行javah命令时,碰到了找不到文件的情况,一开始一头雾水,后来多方查找后发现是目录问题。
切换到class类的顶层目录,再次执行此命令,通过。
2、JNI调用dll报错找不到依赖的类库
|
|
意思是EasemobLib.dll中依赖的dll没有加载进来,这里推荐一类工具,dll依赖查看工具。可以直接看出自己的dll中都依赖哪些dll,里面缺少哪些。再对应的去找。
我也是这个步骤找到了自己没有加入的dll,然后copy到我的jdk的bin中就好了。
3、JNI与C++交互时中文乱码
所谓的乱码,其实就是字符的表示不一致,也就是字符的编码或者说占用的字节长短不一样所造成的。
- C/C++用的都是最原始的数据,一个字符占用一个字节,如果处理中文,一般都是用GB2312,而此时就是1个汉字占用两个自己了。
- JNI中则是使用了UTF-8,一个ASCII字符占1个字节,中文占3个
正是因为对于数据占用字节长度的不一致才造成了乱码现象,因此在本地代码中进行转码即可。
Java—>C++
从Java端传递字符串到C++中时,C++中收到的是jstring。此时可以用JNIEnv中的方法来进行转换。
GetStringUTFChars,得到UTF-8的字符串;
GetStringChars,得到UTF-16的字符串。
然后可以进一步转换为GB2312的编码内容。
C++—>Java
从C++到Java,传递字符串到Java中,不管是回调还是函数返回值,都要进行转换,转为jstring。JNIEnv中也提供了对应的方法来转换。
NewStringUTF,得到UTF-8的jstring;
NewString,得到UTF-16的jstring。
参考资料地址
JNI官方介绍
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/
Android 开发者官网对JNI的介绍
https://developer.android.com/training/articles/perf-jni.html
JNI简要介绍
http://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html#zz-3.
JNI具体内容翻译