`

Android平台Native开发与JNI机制详解

阅读更多

个人认为下面这篇转载的文章写的很清晰很不错. 注意Android平台上的JNI机制使用包括Java代码中调用Native模块以及Native代码中调用Java模块.

 

http://www.ophonesdn.com/article/show/263

 

众所周知,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工程,并添加一个 com.example.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();  
  /* A native method that is implemented by the
     * 'hello-jni' native library, which is packaged
     * with this application.
     */
    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. }  
#include <string.h>
#include <jni.h>
jstring
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
                                                 jobject thiz ) {
        return (*env)->NewStringUTF(env, "Hello from JNI !");
}

 

从函数名可以看出,这个Native函数对应的正是我们在com.example.hellojni.HelloJni这个中声明的Native函数String stringFromJNI()的具体实现。
 
从上面Native函数的命名上我们可以了解到JNI函数的命名规则: Java代码中的函数声明需要添加native 关键 字;Native的对应函数名要以“Java_”开头,后面依次跟上Java的“package名”、“class名”、“函数名”,中间以下划线“_” 分割,在package名中的“.”也要改为“_”。此外,关于函数的参数和返回值也有相应的规则。对于Java中的基本类型如intdoublechar 等,在Native端都有相对应的类型来表示,如jintjdoublejchar 等;其他的对象类型则统统由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. }  
    static {
        System.loadLibrary("hello-jni");
    }
 
注意这里调用的共享库名遵循Linux对库文件的命名惯例,因为OPhone的核心实际上是Linux系统——上例中,实际加载的库文件应为 “libhello-jni.so”,在引用时遵循命名惯例,不带“lib”前缀和“.so”的扩展名。对于没有按照上述惯例命名的Native库,在加 载时仍需要写成完整的文件名。
 
JNI函数的使用方法和普通Java函数一样。在本例中,调用代码如下:
 
  1. TextView tv =  new  TextView( this );  
  2. tv.setText( stringFromJNI() );  
  3. setContentView(tv);  
    TextView tv = new TextView(this);
    tv.setText( stringFromJNI() );
    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  com.example.hellojni;  
  2. public   class  SayHello {  
  3.         public  String sayHelloFromJava(String nativeMsg) {  
  4.                String str = nativeMsg + " But shown in Java!" ;  
  5.                return  str;  
  6.         }  
  7. }  
package com.example.hellojni;
public class SayHello {
        public String sayHelloFromJava(String nativeMsg) {
               String str = nativeMsg + " But shown in Java!";
               return str;
        }
}
 
接下来要实现的就是在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. }  
jstring helloFromJava( JNIEnv* env ) {
       jstring str = NULL;
       jclass clz = (*env)->FindClass(env, "com/example/hellojni/SayHello");
       jmethodID ctor = (*env)->GetMethodID(env, clz, "<init>", "()V");
       jobject obj = (*env)->NewObject(env, clz, ctor);
       jmethodID mid = (*env)->GetMethodID(env, clz, "sayHelloFromJava", "(Ljava/lang/String;)Ljava/lang/String;");
       if (mid) {
              jstring jmsg = (*env)->NewStringUTF(env, "I'm born in native.");
              str = (*env)->CallObjectMethod(env, obj, mid, jmsg);
       }
       return str;
}
 
 
可以看到,上述代码和前面讲到的步骤完全相符。这里提一下编程时要注意的要点: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可以在http://developer.android.com/sdk/ndk/index.html 下载。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 /APPLICATION-MK.txt。接下来,我们将示例文件与代码如图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>/docs/ANDROID-MK.txt文件,其中详细描述了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)  
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    :=  hello-jni
LOCAL_C_INCLUDES :=  $(LOCAL_PATH)/include
LOCAL_SRC_FILES   :=  src/call_java.c \
                                          src/hello-jni.c 
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/data/com.example.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的魅力!
 
 
图4 Hello-JNI在OPhone模拟器上的运行结果
参考文献
[1]      Sheng Liang. Java Native Interface: Programmer's Guide and Specification. http://java.sun.com/docs/books/jni/ .
[2]      Sun Microsystems. Java Native Interface Specification v1.5. http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/jniTOC.html .
分享到:
评论
1 楼 chenglnb 2014-08-30  
不错,有收获

相关推荐

    android jni详解

    Android调用C++接口调用技术Java Native Interface(JNI)详解,

    Android系统原理与开发要点详解,多点触屏,移动图片,JNI

    Android系统原理与开发要点详解,muti-touch多点触屏,移动图片,Java Native Interface Specification

    jni参数详解

    关于android开发中 native层用到的jni参数方面的讲解,让大家更清楚jni参数的含义

    Android JNI详解,让你彻底了解JNI (上)

    Android开发中,随着对移动程序的安全性、性能等方面的重视,JNI技术也越发重要。如今,多数企业在招聘中、高级程序员时,基本上都要求熟悉JNI开发,所以,掌握JNI技术,也是我们迈进心仪企业的必备条件。本套课程共...

    详解如何使用Android Studio 进行NDK开发和调试

    尽管Android Studio已经越来越流行了,但很多人还是习惯于Eclipse或源码环境下开发JNI应用。个人认为使用Android Studio作NDK开发是必然趋势,所以本文将简单介绍如何在Android Studio上实现NDK开发。 简介 JNI JNI ...

    android studio 3.4配置Android -jni 开发基础的教程详解

    首先下载配置android studio ndk 1.打开sdkManager下载CMake和LLDB 2.配置ndk 项目新建 项目建立完毕后,工程目录如下,cpp文件夹是系统自动生成的 3.自定义 navite方法 接下来开始写自定义的一个native方法,...

    《深入理解Android》卷Ⅰ

    9.2 Vold的原理与机制分析 9.2.1 Netlink和Uevent介绍 9.2.2 初识Vold 9.2.3 NetlinkManager模块分析 9.2.4 VolumeManager模块分析 9.2.5 CommandListener模块分析 9.2.6 Vold实例分析 9.2.7 关于Vold的总结 9.3 ...

    Android使用jni调用c++/c方法详解

    JniTest是在Android.mk里约束好的,关于Android.mk的编写具体在后面详解。 3、使用javah -jni生成.h文件 编写好jni加载类之后,就要开始生成.h文件了,此文件相当于一个声明文件,起到jni连接c++源

    android-framework-hal

    1.Android OS 开发环境建立与编译 1.1 Android 框架(framework)架构介绍 1.2 如何取得 Android 原始码 1.3 Android product 分支建立 1.4 编译 ARMv5+ Android 系统 (image制作) 1.5 使用 Android emulator 1.6 ...

    详解AndroidStudio JNI +Gradle3.0以上JNI爬坑之旅

    JNI——(Java Native Interface),他是java平台的特性,不是安卓系统提供的。他定义了一些JNI函数,来让开发者可以通过调用这些函数来实现java代码调用C/C++代码。 2.如何使用JNI呢? 我们先将写好的C/C++代码...

    Android jni ndk 工程

    Android studio 编译第一个NDK程序的详细教程的代码,附上Android studio编写第一个NDK工程的过程详解文章的链接https://blog.csdn.net/weixin_37639900/article/details/89680670

    基于Android studio3.6的JNI教程之helloworld思路详解

    一定要选择Native c++类型,最后要选c++11支持。 SDK设置: File-&gt;Settings File-&gt;Project Structure 首先确定工程的目录结构,然后尝试运行一下工程,使用模拟器,确保工程没问题, 在MainActivity的同级目录,...

    Android Jni的简单使用详解

    这时候我们就可以利用Jni(Java Native Interface)来存储我们这些常量值,虽然也不是最安全的方式,但是也增加了反编译和抓包的难度。相对来说还是安全一点的。 具体做法就是: ① 新建一个工程,包名和你需要调用...

    Android开发中libs和jinLibs文件夹的作用详解

    相信各位Android开发中们在Android 开发中经常和这两个文件夹打交道,以前一直迷迷糊糊的使用,没去想过。最近遇到了一些问题,仔细研究了一下,特此记录分享。下面话不多说了,来一起看看详细的介绍吧。 libs: ...

    基于Android studio3.6的JNI教程之opencv实例详解

    (1)新建工程OpenCVDemo,选择,一定要选择Native c++类型,最后要选c++14支持。 (2)File-&gt;Project Structure-&gt;SDK Location,设置这3个路径,NDK选择r14b。 (3)任意找一张图片,复制到res/drawable。 (4)修改布局文件...

    浅谈CMake配置OpenCV 时静态链接与动态链接的选择

    方法: ... 您可能感兴趣的文章:Android Studio中通过CMake使用NDK并编译自定义库和添加预编译库详解Android JNI的基本使用(CMake)详解Android studio ndk配置cmake开发native Ccmake 学习笔记CentO

Global site tag (gtag.js) - Google Analytics