初识JNI

  众所周知,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基础概念

  1. JavaVM、JNIEnv、Jobject
  • JavaVM

  提供调用接口的函数,可以用来获取JNIEnv,可以跨线程使用。

  • JNIEnv

  JNI Environment,用此类型的实例便可以调用JNI提供的函数。它提供了大多数的JNI函数,是一个线程内的本地变量,无法跨线程访问。如果不得不在其他线程引用JNIEnv,则可以使用JavaVM的GetEnv来获取JNIEnv。

  • Jobject

  Jobject是本地方法中访问的Java中的具体对象。

  1. 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方法和回调方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* JNI测试类
*/
public class JNIDemo {
/**
* 无返回值,无参方法测试
*/
public native void test1();
/**
* 有返回值,有参方法测试
* @param strArg
* @return
*/
public native String test2(String strArg);
/**
* C++程序回调本方法,并传递数据到此
* @param strArg
*/
public void receiveCPPTest(String strArg){
System.out.println("来自C++程序的回调,数据:" + strArg);
}
}

2、把Java源文件编译为class文件

1
javac -encoding UTF-8 JNIDemo.java

  编译java文件的时候我加了一个参数encoding,因为我的电脑默认编码时GBK,而我用的Java文件编码为UTF-8,所以指定UTF-8编码来编译,避免编码不一致而出错的问题。

3、用javah命令把class文件编译为头文件

1
javah JNIDemo

此命令把class文件编译为C++的头文件。

头文件JNIDemo.h中的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNIDemo */
#ifndef _Included_JNIDemo
#define _Included_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JNIDemo
* Method: test1
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JNIDemo_test1
(JNIEnv *, jobject);
/*
* Class: JNIDemo
* Method: test2
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JNIDemo_test2
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif

  可以看出,头文件中已经自动导入了jni.h文件,我们在Java文件中声明的native方法也自动用C++语言声明了出来。而没有被native修饰的方法不会被识别。

  生成的函数名明明规则为: Java_Java类名_Java中的方法名。

  如果这个类是存在于一个包中,则生成的头文件中的方法名还会加入此Java类的包目录,因此,头文件中的方法声明规则如下:

1
2
3
JNIEXPORT 返回值类型 JNICALL Java_包目录_类名_方法名(JNI的参数);
JNIEXPORT jstring JNICALL Java_com_example_jni_JNIDemo_test2(JNIEnv *, jobject, jstring);

  此处的参数只是声明,并没有给出参数的标识,在自己编写源码文件时,应该都加上。

4、用对应的本地语言实现头文件中的函数

  源码文件JNIDemo.cpp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "stdafx.h"
#include "JNIDemo.h"
#include <iostream>
#include "StringUtils.h"
using namespace std;
/*
* Class: JNIDemo
* Method: test1
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JNIDemo_test1
(JNIEnv *env, jobject obj)
{
cout << "Hello World, this is C++ printer." << endl;
}
/*
* Class: JNIDemo
* Method: test2
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JNIDemo_test2
(JNIEnv *env, jobject obj, jstring strArg)
{
// 打印来自Java的字符串
cout << StringUtils::jstring2str(env, strArg) << endl;
jstring returnstr = StringUtils::str2jstring(env, "From C++ dll.");
// 向Java端返回字符串
return returnstr;
}

5、生成动态库,放到对应目录下

  本程序中,在Windows下,生成了dll文件,放到了JDK的bin中

6、加载动态库,调用Java类中对应的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* JNI调用测试
*/
public class JNITest {
public static void main(String[] args) {
// 加载C++编译的动态类库,
// 不用写.dll,直接写文件名字即可,这样是为了跨平台,linux上和windows上的类库后缀不一样
System.loadLibrary("JavaCpp");
JNIDemo jniDemo = new JNIDemo();
// 1、测试无参无返回值的方法
jniDemo.test1();
// 2、测试有参有返回值的方法。
String test2Result = jniDemo.test2("From Java");
System.out.println("-----test2---begin---");
System.out.println("strArg-->" + test2Result);
System.out.println("-----test2---end---");
}
}

7、运行

运行main方法,得到如下输出内容

注意

  以上的例子是我在研究完工作内容后做的,工作工程中针对此部分还碰到了很多问题,有几个比较典型的,做一下记录。

1、class文件到头文件

  在class的目中执行javah命令时,碰到了找不到文件的情况,一开始一头雾水,后来多方查找后发现是目录问题。

  切换到class类的顶层目录,再次执行此命令,通过。

2、JNI调用dll报错找不到依赖的类库

1
2
3
java.lang.UnsatisfiedLinkError:
D:\develop\Java\1.8\32\jdk1.8.0_65\bin\EasemobLib.dll:
Can't find dependent libraries

  意思是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具体内容翻译

http://blog.csdn.net/android_hasen/article/details/27679165

大爷给小弟的零花钱
显示 Gitment 评论