在Android开发中,有时候我们需要依赖设备上的某些特定功能,例如打印功能,那么就需要调用一些“so”文件,这样才能使用打印功能。这些“so”文件其实是用c/c++编写的代码,而Java为这样的开发需求提出了自己的标准,就是 JNI开发。
本篇文章只是简单介绍JNI开发有关知识,如果需要开发JNI,请使用NDK以及查阅有关JNI开发文档。
一、概述。
1.什么是JNI?
JNI全称为Java Native Interface(JAVA本地调用)。从Java1.1开始,JNI成为java平台的一部分,它允许Java代码和其他语言写的代码(如C&C++)进行交互。并非从Android发布才引入JNI的概念的。
JNI是一个协议,这个协议用来沟通java代码和外部的本地代码(c/c++),外部的c/c++代码也可以调用java代码。JNI开发属于Java开发的范围,有关JNI开发,就得参考Java开发文档了!
2.为什么使用JNI?
(1).效率上 C/C++是本地语言,比java更高效;
(2).代码移植,如果之前用C语言开发过模块,可以复用已经存在的c代码;
(3).java反编译比C语言容易,一般加密算法都是用C语言编写,不容易被反编译。
二、JNI的数据类型。
我们知道Java的数据类型是跟C/C++的数据类型是不一样的,而JNI是处于Java和Native本地库(大部分是用C/C++写的)中间的一层,JNI对于两种不同的数据类型之间必须做一种转换,所以在JNI跟Java之间就会有数据类型的对应关系。
JNI的数据类型包含两种:基本类型和引用类型。基本类型主要有jboolea、jchar、jbyte等,他们和Java中的数据类型的对应关系如下图所示,
JNI中的引用类型主要有类、对象和数组,他们和Java中的引用类型的对应关系如下图所示,
三、JNI类型签名。
JNI方法签名。
为什么会有方法签名这种东西呢?这是因为Java这边支持函数重载,即虽然参数不一样,但是方法名一样,那么在JNI层它们的方法名都会是一样的,那JNI也会犯迷糊了,得找哪个呢?
不过也正是因为其参数类型是不一样的,所以就出现了方法签名,利用方法签名和方法名来唯一确定一个JNI函数的调用。
既然方法签名是基于参数类型的不同而形成的,首先要知道Java各数据类型对应的签名是什么,也就是所谓的类型签名。
JNI的类型签名标识了一个特定的Java类型,这个类型即可以是类和方法,也可以是数据类型。
1.类的签名比较简单,它采用“L+包名+类名+;”的形式,只需要将其中的"."替换为"/"即可。比如java.lang.String,它的签名为Ljava/lang/String;,注意末尾的";"也是签名的一部分。
2.基本类型的签名采用一系列大写字母来标识,如下图所示,
基本数据类型的签名是有规律的,一般为首字母的大写,但是boolean和 long比较特殊,在使用时需要留意。
3.对象和数组签名。
对象和数组的签名复杂一些。对于对象来说,它的签名就是对象所属的类的签名,比如String对象,它的签名为Ljava/lang/String;。
对于数组来说,其为 : [ + 其类型的域描述符。比如int数组,其类型为int,而int的签名为I,所以int数组的签名就是[I,例如,
int[ ] 其描述符为[I
float[ ] 其描述符为[F
String[ ] 其描述符为[Ljava/lang/String;
Object[ ]类型的域描述符为[Ljava/lang/Object;
对于多维数组则是
n个[ +该类型的域描述符 , N代表的是几维数组。例如:
int [ ][ ] 其描述符为[[I
float[ ][ ] 其描述符为[[F
4.方法签名。
方法的签名为(参数类型签名)+ 返回值类型签名。对于,没有返回值的,用V(表示void型)表示。举个例子,
Java层方法 JNI函数签名
String test ( ) Ljava/lang/String;
int f (int i, Object object) (ILjava/lang/Object;)I
void set (byte[ ] bytes) ([B)V
四、JNI函数命名规则。
javah生成的c/c++头文件的时候,会对java中定义的 native 函数生成对应的jni层函数,如下:
#include <cn_xinxing_jnitest_CalculateUtils.h>
JNIEXPORT jstring JNICALL Java_cn_xinxing_jnitest_CalculateUtils_getStringFromNative
(JNIEnv * env, jobject obj, jstring str){
return str;
}
首先函数名的格式遵循如下规则:
Java_包名_类名_方法名。比如 Java_cn_xinxing_jnitest_CalculateUtils_getStringFromNative
(JNIEnv * env, jobject obj, jstring str),其中cn_xinxing_jnitest是包名,CalculateUtils是类名,getStringFromNative是方法名,jstring 是方法的String类型的参数。JNIEXPORT、JNICALL、JNIEnv和jobject都是JNI标准中所定义的类型或者宏,它们的含义如下:
(1).JNIEnv * :表示一个指向JNI环境的指针,可以通过它来访问JNI提供的接口方法;
(2).jobject: 表示Java对象种的this;
(3).JNIEXPORT和JNICALL: 它们是JNI中所定义的宏可以在jni.h这个头文件中查找到。
如果不这样命名,当把动态库加载进DVM的时候,通过JNIEnv *指针去查找Java Native方法对应的JNI方法的时候,就会找不到了。
五、JNI 调用流程。(出自 Android的NDK开发(1)————Android JNI简介与调用流程)
众所周知,Android的应用层的类都是以Java写的,这些Java类编译为Dex文件之后,必须靠Dalvik虚拟机( Virtual Machine)来执行。假如在执行java程序时,需要载入C&C++函数时,Dalvik虚拟机就会去加载C&C++的库,(System.loadLibrary("libName");)让java层能顺利地调用这些本地函数。需要清楚一点,这些C&C++的函数并不是在Dalvik虚拟机中运行的,所以效率和速度要比在Dalvik虚拟机中运行得快很多。
Dalvik虚拟机成功加载库之后,就会自动地寻找库里面的JNI_OnLoad函数,这个函数用途如下:
(1)告诉Dalvik虚拟机此C库使用哪一个JNI版本。如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。但是新版的JNI做了很多的扩充,也优化了一些内容,如果需要使用JNI的新版功能,就必须在JNI_OnLoad()函数声明JNI的版本。如
result = JNI_VERSION_1_4;
当没有JNI_OnLoad()函数时,Android调试信息会做出如下提示(No JNI_OnLoad found)
04-29 13:53:12.184: D/dalvikvm(361): Trying to load lib /data/data/com.conowen.helloworld/lib/libHelloWorld.so 0x44edea98
04-29 13:53:12.204: D/dalvikvm(361): Added shared lib /data/data/com.conowen.helloworld/lib/libHelloWorld.so 0x44edea98
04-29 13:53:12.204: D/dalvikvm(361): No JNI_OnLoad found in /data/data/com.conowen.helloworld/lib/libHelloWorld.so 0x44edea98, skipping init
(2)因为Dalvik虚拟机加载C库时,第一件事是调用JNI_OnLoad()函数,所以我们可以在JNI_OnLoad()里面进行一些初始化工作,如注册JNI函数等等。注册本地函数,可以加快java层调用本地函数的效率。
另外:与JNI_OnLoad()函数相对应的有JNI_OnUnload()函数,当虚拟机释放该C库时,则会调用JNI_OnUnload()函数来进行善后清除动作。
六、总结。
本篇文章只是简单介绍了JNI的基础知识,文章是笔者读书以及阅读其他文章的一个总结。如果要开发JNI,请看我的下篇文章,你不知道的Android NDK开发。部分内容来自《Android开发艺术探索》