jni
一、引言
1.1 功能简介和编写目的
JNI 是 Java Native Interface 的缩写,它提供了一些 API 来实现 Java 和其他语言(主要是 C/C++)的交互,即 Java 与操作系统本地代码之间可以互相调用,JNI 是 Java 平台的一个特性(并不是 Android 系统特有的)。
Java 不是完美的,Java 的不足除了体现在运行速度上要比传统的 C/C++ 慢许多之外,Java 无法直接访问到操作系统底层(如系统硬件等),为此 Java 使用 native 方法来扩展 Java 程序的功能。
1.2 适用范围
本文涉及的代码,native 方法用 C 实现,并且是 Linux 环境下的 C 编程。
1.3 参考资料
JNI 官方手册 https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
《深入理解 android 卷一》第二章 http://blog.csdn.net/innost/article/details/47204557
二、JNI开发说明
2.1 JNI开发流程图
2.2 JNI代码示例
2.2.1 java 调用 C 方法来打印 helloworld
java 代码部分,先写一个类 TestJni,把 main 方法写好。因为需要调用 C 的方法,所以需要先声明一下 ative 方法,注意关键字 natvie,它表示该方法不是 java 代码实现的,而是本地代码(C/C++)实现的,另外调用 native 方法之前需要先加载动态库,对于 Linux 系统来说,动态库的形式是 libxxx.so,调用 loadLibrary 方法加载动态库时不需要前缀 lib 和后缀 .so。
java 代码部分写好后,使用 javac 命令编译出 java 字节码文件,再使用 javah 命令生成一个 C/C++ 使用的头文件。
接下来根据上面生成的头文件来实现 C 代码部分,我们要实现打印一个 helloworld,只需写一行代码即可。
上图中第4行代码看着比较奇怪,具体的解释请看下面。
JNIEXPORT:它的定义在jni_md.h(jni.h包含这个头文件)中,在linux环境下,如果使用的是GCC编译器,并且GCC编译器的版本号大于等于4.2,那么它的定义为__attribute__((visibility("default"))),否则它的定义为空。__attribute__((visibility("default")))用于设置动态库中函数的可见性,default表示可见,如果取值hidden表示不可见。
JNICALL:它的定义在jni_md.h(jni.h包含这个头文件)中,在linux环境下,它的定义为空,一般来说宏定义为空是为了方便代码在不同平台进行移植。
友情提示:以上两个宏JNIEXPORT和JNICALL大家不必深究,当成固定格式记住就行了。
void:这个很好理解,表示native方法的返回值为空。
Java_TestJni_print:这是JNI函数名,命名规范为Java_包名_类名_方法名,因为我编写的java代码没有导入包,所以这里没有包名。
**JNIEnv ***:JNIEnv *表示JNI接口指针,它指向了一个函数表,函数表中的每一个入口指向一个JNI函数,我们可以调用这些函数对Java代码进行操作,例如:创建Java对象,调用Java对象方法,获取Java对象属性等。
jobject:jobject相当于Java中的Object类型,它代表调用这个本地方法的对象。如果是非静态方法,那么jobject是对对象的引用,如果是静态方法,则jobject是对它的class类的引用。
根据上面的JNI开发流程,C代码写完后需要编译出一个动态库。注意图中的-I参数,因为TestJni.h中包含了一个jni.h,而这个头文件不在gcc编译时默认的头文件搜索路径下,它保存的路径实际上是在JDK的安装路径下,所以这里加个-I参数指定一下头文件搜索路径。\
最后使用java命令运行一下即可看到效果,-Djava.library.path=./表示指定动态库搜索路径为当前目录。
2.2.2 C回调java方法打印helloworld
上面介绍了一个最简单的JNI示例,java调用C的方法来打印helloworld。下面介绍一下C回调java的方法来打印helloworld。
首先同样先写好java部分的代码,为了以示区分是C回调的java方法,我们把方法名叫做callbackPrint,并且打印的时候也加个”java print”表示这是java打印的。
和上面那个例子一样,编译一下java代码,生成JNI头文件,其实这里可以不用再生成一次头文件,因为并没有增加native方法,现在我们只有一个native方法:print。
接下来实现C代码的部分,这里就稍微复杂一点,总共需要做三步,
(1)调用FindClass函数获取java类,该函数第一个参数是前面提到的JNI接口指针,第二个参数是包名加类名。在Java中,Class类代表一个Java类编译的字节码,即:这个Java类,里面包含了这个类的所有信息。在JNI中,同样定义了这样一个类:jclass。
(2)调用GetMethodID函数来获取指定类中的方法ID,第一个参数同样是JNI接口指针,第二个参数是上一步获取的jclass,第三个参数是类中的方法名,第四个参数是方法签名,关于签名的详细说明,请看2.3.4节。
(3)调用CallVoidMethod函数来调用java方法,这里由于java方法的返回值为空,所以是CallVoidMethod,如果返回值是int,那么就是CallIntMehtod,其它类型的以此类推。
最后编译出动态库,运行后即可看到效果。
2.3 JNI参数传递和方法签名
2.3.1 JNI数据类型映射
Java基本数据类型 JNI类型 C/C++类型(linux)
boolean jboolean unsigned char
byte jbyte signed char
char jchar unsigned short
short jshort short
int jint int
long jlong long long
float jfloat float
double jdouble double
Java引用数据类型 JNI类型 描述
Object jobject 任何java对象
Class jclass Class类对象
String jstring 字符串对象
Object[] jobjectArray 任何java对象数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 字节型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 单精度浮点型数组
double[] jdoubleArray 双精度浮点型数组
注意:java基本数据类型传到本地可以直接使用,java引用数据类型传到本地需要调用JNI函数经过一定的转换才能使用。
2.3.2 java基本数据类型的参数传递
下面写一个java调用C方法来计算两个整数之和的例子,先声明一个native方法,方法名就叫sum,下面调用该方法即可。
同样编译出class文件,再生成jni头文件。
接下来根据头文件实现C的部分,实现的话很简单,对于java传过来的基本数据类型我们可以直接用,所以我们直接return a+b可。
最后编译出动态库,运行即可看到效果。
2.3.3 java引用数据类型的参数传递
java引用数据类型的参数传递就比较复杂了,我们不能直接用,需要经过一定的转换。下面举个例子,java传递一个String对象给C,C先将String对象转换成字符串(char *),再将该字符串进行逆序处理,最后转换成String对象返回给java。
同样先实现java的部分,声明一个native方法,名字叫做stringReverse(reverse的意思是颠倒),下面再调用该方法即可。
编译出class文件,生成jni头文件。
下面实现C的部分,我们需要先把java传过来的String对象转换成C可以用的字符串,调用GetStringUTFChars即可,然后把C字符串进行逆序处理,保存在一个buf里面,接下来需要调用ReleaseStringUTFChars把刚才转换的字符串给释放掉,最后将buf转换成String对象返回给java即可。
编译出动态库,运行即可看到效果。
2.3.4 JNI方法签名
2.2.2节我们调用了GetMethodID函数来获取java类的方法ID,该函数最后一个参数是方法签名,签名的形式如下:
(参数1类型签名参数2类型签名参数3类型签名....…)返回值类型签名
参数类型对应的签名如下表:
参数类型 类型签名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
注意:1.每个参数的类型签名紧紧挨着,中间没有逗号,没有空格。
2.如果参数是Java类,以”L”开头,以”;”结尾,中间用”/“隔开包及类名。
3.如果参数是数组,以[开头,后面跟数组元素类型的签名。
4.返回值签名在括号后面,如果返回值为空,写上V。
例如:
java方法 对应签名
boolean fun( ); ( )Z
void fun(int a); (I)V
char fun(int a, float b, String c) (IFLjava/lang/String;)C
String fun(int a, String b, double[ ] c) (ILjava/lang/String;[D)Ljava/lang/String;
2.5 异常场景/注意事项说明
1.JNI接口指针JNIEnv只在当前线程有效,因此在本地方法中不要跨线程传递接口指针参数。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNIEnv。
2.JNI的缺点。
答:(1)程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。 (2)程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。
2.6 常见问题汇总
- C/C++代码编写完成后编译动态库时报错,提示找不到jni.h,如下图。
答:因为jni.h不在gcc编译时默认的头文件搜索路径下,而是放在jdk的安装目录下,所以gcc编译时需要指定头文件搜索路径,可采用如下的方式(注意下面指定的两个路径):
gcc -I /usr/lib/jvm/java-7-openjdk-amd64/include/ -I testjni.c -fPIC -shared -o libtestjni.so
- 使用java命令运行时报错,提示找不到testjni动态库,如下图。
答:因为libtestjni.so不在linux系统默认动态库搜索路径下(/lib和/usr/lib),所以运行时会报错。有三种方法可以解决这个问题:
(1) 将libtestjni.so放在/lib或者/usr/lib下,需要sudo权限,不推荐。
(2) 配置linux环境变量LD_LIBRARY_PATH,例如
export LD_LIBRARY_PATH=/home/lichen/tmp/jni
(3) 使用java命令时指定动态库搜索路径,例如
java -Djava.library.path=./ TestJni
3.使用JNI后,程序运行直接崩溃。
答:崩溃的原因一般出在本地代码(C/C++)侧,除了要检查是否存在空指针引用、数组越界等情况,还要看调用JNI函数时是否正确,特别是参数中有方法名和方法签名的情况,如果这两个写错了,编译是不会报错的,只有在运行时会崩溃,比如方法签名是()V,你写成()v就会崩溃。