JNI技术总结
目 录
1概述
1.1简介
Java本机接口(JavaNative Interface (JNI))是一个本机编程接口,它是Java软件开发工具包(JavaSoftwareDevelopment Kit(SDK))的一部分,它提供了若干的API,实现了和Java和其他语言的通信(主要是C&C++)。
JNI允许Java代码使用以其它语言(譬如C和C++)编写的代码和代码库。
同时,我们知道Java是一种平台无关性的语言,平台对于上层的java代码来说是透明的,所以在多数时间我们是不需要JNI的,但是当你遇到如下三种情况:
1.你的Java代码,需要得到一个文件的属性。但是你找遍了JDK帮助文档也找不到相关的API。
2.在本地还有一个别的系统,不过他不是Java语言实现的,这个时候你的老板要求你把两套系统整合到一起。
3.你的Java代码中需要用到某种算法,不过算法是用C实现并封装在动态链接库文件当中的。
对于上述的三种情况,如果没有JNI的话,那就会变得异常棘手。就算找到其他解决方案,也比较费时费力,且会增加开发和维护的成本。
1.2系统环境
系统环境是指本地操作系统环境,它有自己的本地库和CPU指令集,本地程序【NativeApplication】是使用C/C++语言编写的,然后被编译成能在本地系统环境中运行的二进制代码,并和本地库链接在一起。所以本地程序和本地库一般都会依赖于一定的本地系统环境,也就是说在一个系统环境中编译出来的C程序无法在另一个系统中运行。所以当我们需要在多个系统中统一对上层的接口时,JNI的作用就体现出来了。
JNI最常见的两个应用:从Java程序调用C/C++,以及从C/C++程序调用Java代码.
2JNI的作用
2.1JNI的角色
由于JNI的强大特性,允许我们使用JAVA平台的同时,还可以复用之前的本地C/C++代码。作为虚拟机实现的一部分,JNI允许JAVA与本地代码间的双向交互。
其交互框架如下:
JNI与Native程序的交互:
-
使用JNI来实现Native方法,并在JAVA程序中来调用它们.
-
JNI支持一个调用接口【invocationinterface】,允许你把一个JVM嵌入本地程序中,本地程序也可以链接一个实现了JVM的本地库,然后使用“调用接口”从而执行JAVA语言程序。
2.2JNI的副作用
JAVA语言以跨平台而著称,但在使用JNI后,它的跨平台性也就丧失了:
-
程序不再跨平台
如果要跨平台,则需在不同系统环境中对本地代码进行重新编译
-
程序不再绝对安全
本地代码的不当使用可能会导致整个程序或系统崩溃.
为了尽可能减少JNI的副作用,通常让本地方法集中在较少的几个类中,以降低JAVA与本地程序的耦合性。
2.3JNI的使用场景
由于JNI具有副作用,所以在某些非必须使用JNI时,应考虑是否可用其他方法来达到与本地代码交互的目的。如:JAVA与本地代码使用TCP/IP或IPC进行交互等,通常在JAVA程序和本地代码处于不同的线程,或者不同的机器上时,即可不使用JNI。这样在本地程序出现问题时不至于影响到JAVA程序。
但在下面的场景中,同一进程内JNI的使用则无法避免:
-
JAVA程序中使用到了JAVAAPI不提供的特殊系统环境才有的特征,而跨进程操作不现实。
-
需要访问已有的本地库,但又不想付出跨进程调用时的代价,如:效率,内存等
-
JAVA程序中的一部分代码对处理效率要求非常高,如:算法计算,图形渲染等。
3数据类型
在C/C++代码中,编译器会根据所处的平台为一些基本的数据类型分配长度,因此就存在平台的不一致性,而在JAVA中则没有这个问题因为有JVM的存在,基于这个原因,就需要对JAVA和C++中的基本数据类型有一个Mapping,以保持数据类型的一致性。
C/C++与JAVA的基本数据类型映射表如下:
-
JAVA类型
Native类型
JNI别名
Int
long
jint
long
_int64
jlong
byte
signedchar
jbyte
boolean
unsignedchar
jboolean
char
unsignedshort
jchar
short
short
jshort
float
float
jfloat
double
double
jdouble
object
_jobject*
jobject
其实,JNI的设计者已经帮我们取好了相应的别名以方便记忆。如果想了解一些更加细致的信息,可以去看一下jni.h这个头文件,各种数据类型的定义以及别名就被定义在这个文件中。
除了Java声明中的一般参数以外,所有这些函数的参数表中都有一个指向JNIEnv和jobject的指针。
4JNI例程
Android通过JNI来实现Java层调用C层代码,我们必须创建c代码,然后编译so库,编写JNI中的代码,最后Java层通过System.loadLibrary()方法加载so动态库,即可实现。
编写一个具有so,jni , java整体模块,一般可以这样来做:
(1)编写Java层代码,里面主要实现两个步骤,一个是定义native方法,另一个是调用System.loadLibrary()方法加载C层要写的动态库;
(2)由Java代码使用javah命令生成JNI中所需的*.h头文件;
(3)实现以上所生成的*.h头文件;
(4)编写Android.mk文件,生存*.so库
(5)编译整个工程生成apk。
以下将以一个例程为例来进行说明,例程将展示从java调用native以及native调用java的方法,在此例程中,需要在native中实现的方法都是从java调用native的,onNativeCallback(int)这是从native调用java的,在以下的步骤中你将会见到它们.
以下将就这些步骤逐个讲解:
一:编写Java代码
根据我们需要实现的Java程序,来编写native方法和调用System.loadLibrary()方法加载动态库.
此例程中定义了4个native方法,分别是:
1.nativeInitilize():native环境初始化
2.nativeThreadStart(String mstr):启动native线程
3.NativeThreadStop():停止native线程
4.onNativeCallback( int count ):native向java的回调
注意,在Java代码中的System.loadLibrary()中是不需要写完整so名称的,即如果你的so全程是:libnb.so,则在java代码中load该库应该使用System.loadLibrary(nb),因为lib前导在系统加载时会自动添加,否则就会出现错误,由上图可见,此处使用的so名称为:libnative.so.
Java文件的Android.mk文件如下:
二:用javah命令生成JNI中所需的.h头文件
javah命令是将Java源文件生成C/C++头文件,javah命令为:javah–jni包名&类名,并且执行该命令生成头文件需要在src路径下,在命令执行完成后,在当前目录就会发现所生成的.h头文件。
如下:
按照<一>中定义的方法,此处生成的头文件内容为:
可以看到,需要native实现的有三个方法,并且他们的命名也有一定的规律,其实在使用JNI时,如果熟练的话可以略过此步,直接对应编写相应的C/C++实现即可。
三:实现.h头文件,编写c/c++实现
我们要根据前面所生成的.h文件编写c/c++文件来实现该头文件,.c和.cpp文件的名称由用户自己定义,但在Android.mk文件中也要注意相应加入该文件到LOCAL_SRC_FILES,一般情况下,使用和.h相同的名字。
此处使用的文件名是:native.c
四:编写Android.mk文件,生成so动态库
当我们实现了.c或.cpp文件后,编写Android.mk文件,用于将C/CPP生成动态库.
如果使用NDK开发则可使用[ndk-build]来进行编译。
五:编译与调试
但代码编译完成,即可进行单独或完整编译,以检测代码编写的正确性,若编译成功,则可以将编译生成的so和apk导入到设备中或烧录所编译出来的完整版本即可进行调试。
本例程运行结果如下:
只要开发者遵守以上规则进行代码开发,出现问题的几率很小,也就是说jni的调试其实比较简单,所以本文就不再赘述。
5总结
以上主要介绍了JNI的一些基本知识以及使用例程,有了以上的基础,对JNI的使用应该已经没有问题了,以下列出一些在使用时容易出错的地方和使用建议。
5.1JNI使用注意事项
JNI虽然相对简单,但在使用时还是需要注意一些容易出现问题的地方:
-
错误检查
编写本地方法时应注意进行错误检查以在出现异常时快速发现问题
-
JNI参数传递
JNI不会检查参数的正确性,所以在使用JNI时应保证参数的正确和有效性
-
勿混淆jclass和jobject
初期使用jni时,容易将对象【jobject】引用和类【jclass】引用混淆.对象引用对应的是数据或者java.lang.Object及其子类的对象实例,而类引用对应的是java.lang.class的实例.
-
Jboolean会存在数据截取问题
Jboolean是一个0-255的8bitunsigned的C类型,其中0为JNI_FALSE,1-255对应JNI_TRUE.
5.2 JNI使用建议
针对JNI的使用有如下建议:
-
尽量使JAVA与C的接口简单
-
尽量少写本地代码,并保持尽量独立