Java中JIN机制及System.loadLibrary()的执⾏过程
Android平台Native开发与JNI机制详解
个⼈认为下⾯这篇转载的⽂章写的很清晰很不错. 注意Android平台上的JNI机制使⽤包括Java代码中调⽤Native模块以及Native代码中调⽤Java模块.
众所周知,OPhone平台上的应⽤开发主要基于Java语⾔,但平台完全⽀持且提供了⼀定的Native开发能⼒(主要是C/C++),使得开发者可以借助JNI更深⼊的实现创意。本⽂主要介绍OPhone平台的JNI机制和Native模块开发与发布的⽅法。
JNI简介
Java Native Interface(JNI)是Java提供的⼀个很重要的特性。它使得⽤诸如C/C++等语⾔编写的代码可以与运⾏于Java虚拟机(JVM)中的 Java代码集成。有些时候,Java并不能满⾜你的全部开发需求,⽐如你希望提⾼某些关键模块的效率,或者你必须使⽤某个以C/C++等Native语⾔编写的程序库;此时,JNI就能满⾜你在Java代码中访问这些Native模块的需求。JNI的出现使得开发者既可以利⽤Java语⾔跨平台、类库丰富、开发便捷等特点,⼜可以利⽤Native语⾔的⾼效。
图1 JNI与JVM的关系
实际上,JNI是JVM实现中的⼀部分,因此Native语⾔和Java代码都运⾏在JVM的宿主环境(Host Environment),正如图1所⽰。此
外,JNI是⼀个双向的接⼝:开发者不仅可以通过JNI在Java代码中访问Native模块,还可以在 Native代码中嵌⼊⼀个JVM,并通过JNI访问运⾏于其中的Java模块。可见,JNI担任了⼀个桥梁的⾓⾊,它将JVM与Native模块联系起来,从⽽实现了Java代码与Native代码的互访。在OPhone上使⽤Java虚拟机是为嵌⼊式设备特别优化的Dalvik虚拟机。每启动⼀个应⽤,系统会建⽴⼀个新的进程运⾏⼀个Dalvik虚拟机,因此各应⽤实际上是运⾏在各⾃的VM中的。Dalvik VM对JNI的规范⽀持的较全⾯,对于从JDK 1.2到JDK 1.6补充的增强功能也基本都能⽀持。
开发者在使⽤JNI之前需要充分了解其优缺点,以便合理选择技术⽅案实现⽬标。JNI的优点前⾯已经讲过,这⾥不再重复,其缺点也是显⽽易见的:由于Native模块的使⽤,Java代码会丧失其原有的跨平台性和类型安全等特性。此外,在JNI应⽤中,Java代码与Native代码运⾏于同⼀个进程空间内;对于跨进程甚⾄跨宿主环境的Java与Native间通信的需求,可以考虑采⽤socket、Web Service等IPC通信机制来实现。
在OPhone开发中使⽤JNI
正如我们在上⼀节所述,JNI是⼀个双向的接⼝,所以交互的类型可以分为在Java代码中调⽤Native模块
和在Native代码中调⽤Java模块两种。下⾯,我们就使⽤⼀个Hello-JNI的⽰例来分别对这两种交互⽅式的开发要点加以说明。
Java调⽤Native模块
Hello-JNI这个⽰例的结构很简单:⾸先我们使⽤Eclipse新建⼀个OPhone应⽤的Java⼯程,并添加⼀个 ample.hellojni.HelloJni的类。这个类实际上是⼀个Activity,稍后我们会创建⼀个TextView,并显⽰⼀些⽂字在上⾯。
要在Java代码中使⽤Native模块,必须先对Native函数进⾏声明。在我们的例⼦中,打开HelloJni.java⽂件,可以看到如下的声明:
1. /* A native method that is implemented by the
2.    * 'hello-jni' native library, which is packaged
3.    * with this application.
4.    */
5.  public  native  String  stringFromJNI();
Java代码
1. /* A native method that is implemented by the
2.    * 'hello-jni' native library, which is packaged
3.    * with this application.
4.    */
5.  public native String  stringFromJNI();
从上述声明中我们可以知道,这个stringFromJNI()函数就是要在Java代码中调⽤的Native函数。接下来我们要创建⼀个hello-jni.c的C⽂件,内容很简单,只有如下⼀个函数:
1. #include <string.h>
2. #include <jni.h>
3. jstring
4. Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
5.                                                  jobject thiz ) {
6.        return  (*env)->NewStringUTF(env,  "Hello from JNI !" );
7. }
Java代码
1. #include <string.h>
2. #include <jni.h>
3. jstring
4. Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
5.                                                  jobject thiz ) {
6.        return (*env)->NewStringUTF(env, "Hello from JNI !");
7. }
从函数名可以看出,这个Native函数对应的正是我们在ample.hellojni.HelloJni这个中声明的Native函数String stringFromJNI()的具体实现。
从上⾯Native函数的命名上我们可以了解到JNI函数的命名规则: Java代码中的函数声明需要添加native关键字;Native的对应函数名要以“Java_”开头,后⾯依次跟上Java的“package名”、“class名”、“函数名”,中间以下划线“_” 分割,在package名中的“.”也要改为“_”。此外,关于函数的参数和返回值也有相应的规则。对于Java中的基本类型如int、double、char等,在Native端都有相对应的类型来表⽰,如jint 、jdouble、jchar等;其他的对象类型则统统由jobject来表⽰(String是个例外,由于其使⽤⼴泛,故在Native代码中有jstring这个类型来表⽰,正如在上例中返回值String对应到Native代码中的返回值jstring)。⽽对于Java中的数组,在Native中由jarray对应,具体到基本类型和⼀般对象类型的数组则有jintArray等和jobjectArray分别对应(String数组在这⾥没有例外,同样⽤jobjectArray表⽰)。还有⼀点需要注意的是,在JNI的Native函数中,其前两个参数JNIEnv *和jobject是必需的——前者是⼀个JNIEnv结构体的指针,这个结构体中定义了很多JNI的接⼝函数指针,使开发者可以使⽤JNI所定义的接⼝功能;后者指代的是调⽤这个JNI函数的Java对象,有点类似于C++中的this指针。在上述两个参数之后,还需要根据Java端的函数声明依次对应添加参数。在上例中,Java中声明的JNI函数没有参数,则Native的对应函数只有类型为JNIEnv *和jobject的两个参数。
当然,要使⽤JNI函数,还需要先加载Native代码编译出来的动态库⽂件(在Windows上是.dll,在Linux
上则为.so)。这个动作是通过如下语句完成的:
1. static  {
2.    System.loadLibrary("hello-jni" );
3. }
Java代码
1. static {
2.    System.loadLibrary("hello-jni");
3. }
注意这⾥调⽤的共享库名遵循Linux对库⽂件的命名惯例,因为OPhone的核⼼实际上是Linux系统——上例中,实际加载的库⽂件应为“libhello-jni.so”,在引⽤时遵循命名惯例,不带“lib”前缀和“.so”的扩展名。对于没有按照上述惯例命名的Native库,在加载时仍需要写成完整的⽂件名。
JNI函数的使⽤⽅法和普通Java函数⼀样。在本例中,调⽤代码如下:
1. TextView tv =  new  TextView( this );
system的头文件2. tv.setText( stringFromJNI() );
3. setContentView(tv);
Java代码
1. TextView tv = new TextView(this);
2. tv.setText( stringFromJNI() );
3. setContentView(tv);
就可以在TextView中显⽰出来⾃于Native函数的字符串。怎么样,是不是很简单呢?
Native调⽤Java模块
从OPhone的系统架构来看,JVM和Native系统库位于内核之上,构成OPhone Runtime;更多的系统功能则是通过在其上的Application Framework以Java API的形式提供的。因此,如果希望在Native库中调⽤某些系统功能,就需要通过JNI来访问Application Framework提供的API。
JNI规范定义了⼀系列在Native代码中访问Java对象及其成员与⽅法的API。下⾯我们还是通过⽰例来具体讲解。⾸先,新建⼀个SayHello 的类,代码如下:
1. package  ample.hellojni;
2. public  class  SayHello {
3.        public  String sayHelloFromJava(String nativeMsg) {
4.                String str = nativeMsg + " But shown in Java!" ;
5.                return  str;
6.        }
7. }
Java代码
1. ample.hellojni;
2. public class SayHello {
3.        public String sayHelloFromJava(String nativeMsg) {
4.                String str = nativeMsg + " But shown in Java!";
5.                return str;
6.        }
7. }
接下来要实现的就是在Native代码中调⽤这个SayHello类中的sayHelloFromJava⽅法。
⼀般来说,要在Native代码中访问Java对象,有如下⼏个步骤:
1.        得到该Java对象的类定义。JNI定义了jclass这个类型来表⽰Java的类的定义,并提供了FindClass接⼝,根据类的完整的包路径即可得到其jclass。
2.        根据jclass创建相应的对象实体,即jobject。在Java中,创建⼀个新对象只需要使⽤new关键字即可,但在Native代码中创建⼀个对象则需要两步:⾸先通过JNI接⼝GetMethodID得到该类的构造函数,然后利⽤NewObject接⼝构造出该类的⼀个实例对象。
3.        访问jobject中的成员变量或⽅法。访问对象的⽅法是先得到⽅法的Method ID,然后使⽤Call<Type >Method 接⼝调⽤,这⾥Type对应相应⽅法的返回值——返回值为基本类型的都有相对应的接⼝,如CallIntMethod;其他的返回值(包括String)则为CallObjectMethod。可以看出,创建对象实质上是调⽤对象的⼀个特殊⽅法,即构造函数。访问成员变量的步骤⼀样:⾸先 GetFieldID得到成员变量的ID,然后Get/Set<Type >Field读/写变量值。
上⾯概要介绍了从Native代码中访问Java对象的过程,下⾯我们结合⽰例来具体看⼀下。如下是调⽤sayHelloFromJava⽅法的Native代码:
1. jstring helloFromJava( JNIEnv* env ) {
2.        jstring str = NULL;
3.        jclass clz = (*env)->FindClass(env, "com/example/hellojni/SayHello" );
4.        jmethodID ctor = (*env)->GetMethodID(env, clz, "<init>" ,  "()V" );
5.        jobject obj = (*env)->NewObject(env, clz, ctor);
6.        jmethodID mid = (*env)->GetMethodID(env, clz, "sayHelloFromJava" ,  "(Ljava/lang/String;)Ljava/lang/String;" );
7.        if  (mid) {
8.              jstring jmsg = (*env)->NewStringUTF(env, "I'm born in native." );
9.              str = (*env)->CallObjectMethod(env, obj, mid, jmsg);
10.        }
11.        return  str;
12. }
Java代码
1. jstring helloFromJava( JNIEnv* env ) {
2.        jstring str = NULL;
3.        jclass clz = (*env)->FindClass(env, "com/example/hellojni/SayHello");
4.        jmethodID ctor = (*env)->GetMethodID(env, clz, "<init>", "()V");
5.        jobject obj = (*env)->NewObject(env, clz, ctor);
6.        jmethodID mid = (*env)->GetMethodID(env, clz, "sayHelloFromJava", "(Ljava/lang/String;)Ljava/lang/String;");
7.        if (mid) {
8.              jstring jmsg = (*env)->NewStringUTF(env, "I'm born in native.");
9.              str = (*env)->CallObjectMethod(env, obj, mid, jmsg);
10.        }
11.        return str;
12. }
可以看到,上述代码和前⾯讲到的步骤完全相符。这⾥提⼀下编程时要注意的要点:1、FindClass要写明Java类的完整包路径,并将“.”以“/”替换;2、GetMethodID的第三个参数是⽅法名(对于构造函数⼀律⽤“<init>”表⽰),第四个参数是⽅法的“签名”,需要⽤⼀个字符串序列表⽰⽅法的参数(依声明顺序)
和返回值信息。由于篇幅所限,这⾥不再具体说明如何根据⽅法的声明构造相应的“签名”,请参考 JNI 的相关⽂档。
关于上⾯谈到的步骤再补充说明⼀下:在JNI规范中,如上这种使⽤NewObject创建的对象实例被称为“Local Reference”,它仅在创建它的Native代码作⽤域内有效,因此应避免在作⽤域外使⽤该实例及任何指向它的指针。如果希望创建的对象实例在作⽤域外也能使⽤,则需要使⽤NewGlobalRef接⼝将其提升为“Global Reference”——需要注意的是,当Global Reference不再使⽤后,需要显式的释放,以便通知JVM进⾏垃圾收集。
Native模块的编译与发布
通过前⾯的介绍,我们已经⼤致了解了在OPhone的应⽤开发中使⽤JNI的⽅法。那么,开发者如何编译出能在OPhone上使⽤的Native模块呢?编译出的Native模块⼜如何像APK⽂件那样分发、安装呢?
Google于2009年6⽉底发布了Android NDK的第⼀个版本,为⼴⼤开发者提供了编译⽤于Android应⽤的Native模块的能⼒,以及将Native模块随Java应⽤打包为APK⽂件,以便分发和安装的整套解决⽅案。NDK的全称是Native Development Toolkit,即原⽣应⽤开发包。由于OPhone平台也基于Android,因此使⽤Android NDK编译的原⽣应⽤或组件完全可以⽤于OPhone。需要注意的是,Google声称此次发布的NDK仅兼容于Android 1.5及以后的版本,由于OPhone 1.0平台基于Android 1.5之前的版本,虽然不
排除使⽤该NDK开发的原⽣应⽤或组件在OPhone 1.0平台上正常运⾏的可能性,但建议开发者仅在OPhone 1.5及以上的平台使⽤。
最新版本的NDK可以在下载。NDK提供了适⽤于Windows、Linux和MAC OS X的版本,开发者可以根据⾃⼰的操作系统下载相应的版本。本⽂仅使⽤基于Linux的NDK版本做介绍和演⽰。
NDK的安装很简单:解压到某个路径下即可,之后可以看到若⼲⽬录。其中docs⽬录中包含了⽐较详细的⽂档,可供开发者参考,在NDK 根⽬录下的README.TXT也对个别重要⽂档进⾏了介绍;build⽬录则包含了⽤于Android设备的交叉编译器和相关⼯具,以及⼀组系统头⽂件和系统库,其中包括libc、libm、libz、liblog(⽤于Android设备log输出)、JNI接⼝及⼀个C++标准库的⼦集(所谓“⼦集”是指Android对C++⽀持有限,如不⽀持Exception及STL等);apps⽬录是⽤于应⽤开发的⽬录,out⽬录则⽤于编译中间结果的存储。接下来,我们就⽤前⾯的例⼦简单讲解⼀下NDK的使⽤。
进⼊<ndk>/apps⽬录,我们可以看到⼀些⽰例应⽤,以hello-jni为例:在hello-jni⽬录中有⼀个 Application.mk⽂件和⼀个project⽂件
夹,project⽂件夹中则是⼀个OPhone Java应⽤所有的⼯程⽂件,其中jni⽬录就是Native代码放置的位置。这⾥Application.mk主要⽤于告诉编译器应⽤所需要⽤到的 Native模块有什么,对于⼀般开发在⽰例提供的⽂件的基础上进⾏修改即可;如果需要了解更多,可参考
<ndk>/docs /。接下来,我们将⽰例⽂件与代码如图2放置到相应的位置:
图2 Hello-JNI⽰例的代码结构
可以看到,和Java应⽤⼀样,Native模块也需要使⽤Android.mk⽂件设置编译选项和参数,但内容有较⼤不同。对于Native模块⽽⾔,⼀般需要了解如下⼏类标签:
1.        LOCAL_MODULE:定义了在整个编译环境中的各个模块,其名字应当是唯⼀的。此外,这⾥设置的模块名称还将作为编译出来的⽂件名:对于原⽣可执⾏⽂件,⽂件名即为模块名称;对于静态/动态库⽂件,⽂件名为 lib+模块名称。例如hello-jni的模块名称为“hello-jni”,则编译出来的动态库就是libhello-jni.so。
2.        LOCAL_SRC_FILES:这⾥要列出所有需要编译的C/C++源⽂件,以空格或制表符分隔;如需换⾏,可放置“\”符号在⾏尾,这和GNU Makefile的规则是⼀致的。
3.        LOCAL_CFLAGS:定义gcc编译时的CFLAGS参数,与GNU Makefile的规则⼀致。⽐如,⽤-I参数可指定编译所需引⽤的某个路径
下的头⽂件。
4.        LOCAL_C_INCLUDES:指定⾃定义的头⽂件路径。
5.        LOCAL_SHARED_LIBRARIES:定义链接时所需要的共享库⽂件。这⾥要链接的共享库并不限于NDK编译环境中定义的所有模块。如果需要引⽤其他的库⽂件,也可在此处指定。
6.        LOCAL_STATIC_LIBRARIES:和上个标签类似,指定需要链接的静态库⽂件。需要注意的是这个选项只有在编译动态库的时候才有意义。
7.        LOCAL_LDLIBS:定义链接时需要引⼊的系统库。使⽤时需要加-l前缀,例如-lz指的是在加载时链接libz这个系统库。libc、libm和libstdc++是编译系统默认会链接的,⽆需在此标签中指定。
欲了解更多关于标签类型及各类标签的信息,可参考<ndk>/⽂件,其中详细描述了Android.mk中各个标签的含义与⽤法。如下给出的就是我们的⽰例所⽤的Android.mk:
1. LOCAL_PATH := $(call my-dir)
2. include $(CLEAR_VARS)
3. LOCAL_MODULE    :=  hello-jni
4. LOCAL_C_INCLUDES :=  $(LOCAL_PATH)/include
5. LOCAL_SRC_FILES  :=  src/call_java.c \
6.                                          src/hello-jni.c
7. include $(BUILD_SHARED_LIBRARY)
Java代码
1. LOCAL_PATH := $(call my-dir)
2. include $(CLEAR_VARS)
3. LOCAL_MODULE    :=  hello-jni
4. LOCAL_C_INCLUDES :=  $(LOCAL_PATH)/include
5. LOCAL_SRC_FILES  :=  src/call_java.c \
6.                                          src/hello-jni.c
7. include $(BUILD_SHARED_LIBRARY)
写好了代码和Makefile,接下来就是编译了。使⽤NDK进⾏编译也很简单:⾸先从命令⾏进⼊<ndk>⽬
录,执⾏./build/host-setup.sh,当打印出“Host setup complete.”的⽂字时,编译环境的设置就完成了。这⾥开发者需要注意的是,如果使⽤的Linux发⾏版是Debian或者Ubuntu,需要通过在<ndk>⽬录下执⾏bash build/host-setup.sh,因为上述两个发⾏版使⽤的dash shell与脚本有兼容问题。接下来,输⼊make APP=hello-jni,稍等⽚刻即完成编译,如图3所⽰。从图中可以看到,在编译完成后,NDK会⾃动将编译出来的共享库拷贝到Java⼯程的 libs/armeabi⽬录下。当编译Java⼯程的时候,相应的共享库会被⼀同打包到apk⽂件中。在应⽤安装时,被打包在libs/armeabi ⽬录中的共享库会被⾃动拷贝到/data/ample.HelloJni/lib/⽬录;当System.loadLibrary 被调⽤时,系统就可以在上述⽬录寻到所需的库⽂件libhello-jni.so。如果实际的Java⼯程不在这⾥,也可以⼿动在Java⼯程下创建 libs/armeabi⽬录,并将编译出来的so库⽂件拷贝过去。
图3 使⽤NDK编译Hello-JNI
最后,将Java⼯程连带库⽂件⼀同编译并在OPhone模拟器中运⾏,结果如图4所⽰。
通过上⾯的介绍,你应该已经对OPhone上的Native开发有了初步了解,或许也已经跃跃欲试了。事实上,尽管Native开发在 OPhone上不具有Java语⾔的类型安全、兼容性好、易于调试等特性,也⽆法直接享受平台提供的丰富的API,但JNI还是为我们提供了更多的选择,使我们可以利⽤原⽣应⽤的优势来做对性能要求⾼的操作,也可以利⽤或移植C/C++领域现有的众多功能强⼤的类库或应⽤,为开发者提供了充分的施展空间。这就是OPhone的魅⼒!

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。