1.Android与JNI
从编程语言年地,Android Framework由基于Java语言的Java层与基于C/C++语言的C/C++层组成,每个层中的功能模块都是使用相应的语言编写的,并且两个层中的大部分模块之间保持着千丝成缕的联系。以Android GPS子系统为例,如下图所示,它提供终端的地理位置信息。在Android应用程序获取GPS信息时,需要先调用应用程序框架中Location Manager提供的Java API,而后通过调用框架内的GPS库,连接到GPS设备驱动上,应用程序即可获取当前地理位置信息。在此过程中,C/C++层与Java层相互作用、相互配合,共同完成任务。
在Android Framework中,需要提供一种媒介或桥梁,将Java层(上层)与C/C++层(底层)有机地联系起来,使得它们相互协调,共同完成某些任务。在两层之间充当桥梁的就是Java本地接口(JNI,Java Native Interface),它允许Java代码与基于C/C++编写的应用程序和库进行交互操作。
Java提供了一系列接口,允许Java类与使用C/C++等其他编程语言(在JNI中,这些语言被称为本地语言)编写的应用程序、模块、库进行交互操作。比如,在Java类中使用C语言库中的特定函数,或在C语言程序中使用Java类库,都需要借助JNI来完成。
通常在以下几种情况下会使用JNI:
1)注重处理速度
与本地代码相比,Java代码的执行速度要慢一些。如果对某段程序的执行速度有较高的要求,建议使用C/C++编写代码。而后在Java中通过JNI调用基于C/C++编写的部分,常常能够获得更快的运行速度。
2)硬件控制
为了更好地控制硬件,硬件控制代码通常都使用C语言编写。而后借助JNI将其与Java层连接起来,从而实现对硬件的控制。
3)既有C/C++代码的复用
在程序编写过程中,常常会使用一些已经编写好的C/C++代码,既提高了编程效率,又确保了程序的安全性与健壮性。在复用这些C/C++代码时,就要通过JNI来实现。
2.简单的JNI示例
1)第一步:编写Java代码
|
|
2)第二步:编译Java代码,非常简单。而编译的目的其实是为了生成相应的函数签名。
|
|
3)第三步:生成相应的头文件,利用javah即可
|
|
生成的头文件如下:
可以看到,里面包含了jni.h这个头文件以及sayHello这个Java方法对应的本地方法声明。
其中JNIEXPORT,JNICALL都是JNI关键字,表示此函数要被JNI调用,函数原型中必须有这两个关键字,JNI才能正常调用函数。其实,JNIEXPORT,JNICALL两个关键字都是宏定义,它们被定义在JDK_HOME/include/linux/jni_md.h文件中。
JNIEnv*是JNI接口的指针,用来调用JNI表中的各种JNI函数,如下图所示:
函数原型中的第二个默认参数类型jobject也是JNI提供的Java本地类型,用来在C代码中访问Java对象,此参数中保存着调用本地方法的对象的一个引用。例如上面的helloJni.sayHello();中helloJni对象调用了本地方法,上面JNI函数的第二个参数jobject中保存着对helloJni对象的引用。
Java类型与Java本地类型的对应列表如下:
Java类型 Java本地类型 占用内存大小
byte jbyte 1
short jshort 2
int jint 4
long jlong 8
float jfloat 4
double jdouble 8
char jchar 2
boolean jboolean 1
void void -
以上Java本地类型定义都可以在下面的头文件中找到:
- 1)JDK_HOME/include/jni.h
- 2)JDK_HOME/include/linux/jni_md.h
注意,如果是Windows平台,则jni_md.h的位置则不是这个路径。
4)根据头文件编写相应的C/C++代码
C语言代码如下:HelloJni.c 123456789101112131415161718192021222324252627/*+ Class: HelloJni+ Method: printHello+ Signature: ()V*/JNIEXPORT void JNICALL Java_HelloJni_printHello(JNIEnv *env, jobject obj){printf("Hello JNI!\n");}/*+ Class: HelloJni+ Method: printString+ Signature: (Ljava/lang/String;)V*/JNIEXPORT void JNICALL Java_HelloJni_printString(JNIEnv *env, jobject obj, jstring str){const char*content=(*env)->GetStringUTFChars(env,str,0);printf("%s\n",content);return;}5)生成so库
在Linux下gcc的使用一文中对于Linux下gcc的使用进行了详细的阐述,如果不有熟悉的小伙伴,建议先看一下那篇博客。
由于包含的头文件在JDK_HOME/include和JDK_HOME/include/linux下,所以编译成so库的命令如下:
$gcc -shared -fPIC hellojni.c -I /usr/lib/jvm/jdk7/include -I /usr/lib/jvm/jdk7/include/linux -o libhellojni.so
- 6)运行
在Linux下gcc的使用一文中讲解过库路径、文件路径与环境变量的问题,这里存在同样的问题,因而在运行之前先要将当前的路径添加进去:link 123$export LD_LIBRARY_PATH=LD_LIBRARY_PATH:$PWD$export CLASSPATH=$CLASSPATH:$PWD$java HelloJni
之后可看到正确的输出结果。
值得注意的是,上面是以C语言为例子,如果采用C++的话,除了在编译so库时将gcc换成g++外,其它均一样,所以此处不再示例。
3.Java和C相互调用示例
上面举了Java调用C的例子,下面的例子中则包含了C调用Java中的对象及方法等。
首先是Java代码:
编译后通过javah生成相应的头文件,头文件如下所示:
此处采用C++来实现,jnifunc.cpp的代码如下:
程序通过JNI访问Java类/对象的成员变量按如下顺序进行:
- 1)查找含待访问的成员变量的Java类的jclass值;
- 2)获取此类成员变量的jfieldID值。若成员变量为静态变量,则调用名称为GetStaticFieldID()的JNI函数;若待访问的成员变量是普通对象,则调用名称为GetFieldID()的JNI函数;
- 3)使用1,2中获得的jclass与jfieldID值。
下面重点讲解一下GetStaticFieldID()和GetFieldID()函数。
jfield GetStaticFieldID(JNIEnvenv,jclass clazz,const charname,const char*signature);
jfield GetFieldID(JNIEnvenv,jclass clazz,const charname,const char*signature);
其中env-JNI接口指针
clazz-包含成员变量的类的jclass
name-成员变量名
signature-成员方法签名
那么如何获取成员方法签名呢?
在jdk中有一个javap命令(Java反编译器),利用它可轻松获取指定的成员变量或成员方法的签名。
形式: javap [选项] 类名
选项:-s 输出Java签名
-p 输出所有类及成员
比如这里使用javap -s JniFuncMain命令,输出如下:
然后,编译获取so库的命令为:
后面的步骤与前一个示例相似,此处不再赘述。输出结果如下:
从输出结果可看出,Java通过JNI调用了native方法,C++中通过JNI调用了Java中的成员变量及方法。
实际上在Android系统中,也有大量的代码使用了JNI函数,这些代码存在于Android源码的相关目录中。比如在Android 2.2.1的如下目录下就有:
关于这些,后面还会详细讲解,此处先提一下。
4.在C程序中运行Java类
前面示例代码中的主程序都是使用Java语言编写的,通过这些示例,我们学习了在Java代码中如何通过本地方法调用C函数,即使用JNI的方式。在这个小节中,我们将一起学习在由C/C++编写的主程序中如何运行Java类,这也是使用JNI的重要方式。
我们知道,由Java类编译生成的字节码必须运行在Java虚拟机上,那么在C/C++编写的程序中运行Java类或对象还需要Java虚拟机吗?
答案是肯定的。为此,JNI提供了一套Invocation API,它允许本地代码在自身内存区域内加载Java虚拟机。在C代码中,如何使用Invocation API,装载Java类,并运行指定的方法呢?这就是本节要讲解的主要内容。
下面列出的情况就是需要使用Invocation API在C/C++代码中调用Java代码的几种典型情况。
- 1)需要在C/C++编写的本地应用程序中访问用Java语言编写的代码或代码库;
- 2)希望在C/C++编写的本地应用程序中使用标准Java类库;
- 3)当需要把已有的C/C++程序与Java程序组织链接在一起时,使用Invocation API,可以将它们组织成一个完整的程序。
首先是Java程序,非常简单:
然后是C代码(invocationApi.c):
在生成Java虚拟机选项时,使用JavaVMInitArgs和JavaVMOption结构体,它们定义在jni.h文件中,如下所示:
此处由于包含与jvm相关的库,所以编译命令稍微复杂一点:
另外,由于使用到了libjvm.so,所以如果直接./main的话会出现找不到libjvm.so的错误。解决方法也很简单,只要将/usr/lib/jvm/jdk7/jre/lib/amd64/server加入到链接路径即可:
之后运行即可得到”Hello Invocation API!”的输出。
5.直接注册JNI本地函数
通过前面的简单示例学习,我们已经知道Java虚拟机在运行包含本地方法的Java应用程序时,要经过以下两个步骤:
- 1).调用System.loadLibrary()方法,将包含本地方法具体实现的C/C++运行库加载到内存中;
- 2).Java虚拟机检索加载进来的库函数符号,在其中查找与Java本地方法拥有相同签名的JNI本地函数符号。若找到一致的,则将本地方法映射到具体的JNI本地函数。
像简单示例中那样,若JNI支持的功能函数只有一个,Java虚拟机在将本地方法与C运行库中的JNI本地函数映射在一起时,不会耗费很长时间。但在Android Framework这类复杂的系统下,拥有大量的包含本地方法的Java类,Java虚拟机加载相应运行库,再逐一检索,将各个本地方法与相应的函数映射起来,这显然会增加运行时间,降低运行的效率。
为了解决这一问题,JNI机制提供了名为RegisterNatives()的JNI函数,该函数允许C/C++开发者将JNI本地函数与Java类的本地方法直接映射在一起。当不调用RegisterNatives()函数时,Java虚拟机会自动检索并将JNI本地函数与相应的Java本地方法链接在一起。但当开发者直接调用RegisterNatives()函数进行映射时,Java虚拟机就不必进行映射处理,这会极大提高运行速度,提升运行效率。
由于程序员直接将JNI本地函数与Java本地方法链接在一起,在加载运行库时,Java虚拟机不必为了识别JNI本地函数而将JNI本地函数的名称与JNI支持的命名规则进行比对,即任何名称的函数都能直接链接到Java本地方法上。
首先分析System.loadLibrary()方法的执行过程。在Java代码中,调用System.loadLibrary()方法时,Java虚拟机会加载其参数指定的共享库。然后,Java虚拟机检索共享库内的函数符号,检查JNI_OnLoad()函数是否被实现,若共享库中含有相关函数,则JNI_OnLoad()函数就会被自动调用。否则,像前面的libhellojni.so一样,库中的JNI_OnLoad()函数未被实现,则Java虚拟机会自动将本地方法与库内的JNI本地函数符号进行比较匹配。
在加载指定的库文件时,JNI_OnLoad()函数会被自动调用执行,程序开发者若想手工映射本地方法与JNI本地函数,需要在JNI_OnLoad()函数内调用RegisterNatives()函数进行映射匹配。
jint JNI_OnLoad(JavaVMvm,voidreserved) 其中vm:JavaVM接口指针 reserved:预定参数
若执行成员,则返回所生成的数组引用,否则返回NULL.
仍以HelloJni那个例子为例,HelloJni.java的代码不需要改变,但是用hellojni.cpp代替hellojni.c,其中hellojni.cpp的代码如下:
由于是采用C++,所以编译要采用g++,编译命令如下:
编译时会有一些Warning,可忽略。
在运行之前,需要利用export将当前目录加入到链接库路径中:
然后就可以直接运行了:
可看到正确的输出,说明本地方法调用成功。