牛骨文教育服务平台(让学习变的简单)
博文笔记

Android平台上的JNI技术介绍

创建时间:2014-01-23 投稿人: 浏览次数:5446

http://liuyix.org/blog/2013/android-ndk-and-jni-introduction/


NDK简介

Android是由Google领导开发的操作系统,Android依靠其开放性,迅速普及,成为目前最流行的智能手机操作系统。

/images/android-system-architecture.jpg

图0-1 Android系统架构图

图0-1是Android系统架构图。

大多数程序位于最上层的Java Application层。Android通过把系统划分为几个层次从而使得开发者可以使用平台无关的Java语言进行Android应用开发,不必关心程序实际的硬件环境。 Google不仅为开发者提供了SDK开发套件,为了能让开发者使用C/C++编写的本地化的共享库,利用编译后的共享库更高效的完成计算密集型的操作来提高应用的性能,或者移植重用已有的C/C++组件,提高开发效率,Android 1.5之后,又推出了NDK(Native Development Kit)。有了NDK,开发者能够在Android平台上使用JNI(Java Native Interface)技术,实现应用程序中调用本地二进制共享库。 由于Android系统不同于以往的JNI使用环境而是在嵌入式硬件环境下,Android NDK提供了一套交叉编译工具链,和构建程序的工具方便开发者在桌面环境下编译目标平台的二进制共享库。 目前NDK提供了对ARMv5TE,ARMv7-A,x86和MIPS指令集平台的支持,同时在本地接口的支持上,目前以下本地接口支持

  • libc
  • libm
  • libz
  • liblog
  • OpenGL ES 1.1 and OpenGL ES 2.0 (3D graphics libraries) headers
  • libjnigraphics (Pixel buffer access) header (Android 2.2 以上可用).
  • C++头文件的一个子集
  • Android native应用API接口
  • JNI头文件接口

由上面的介绍,我们可以知道,实际上NDK开发是以JNI技术为基础的,因此要求开发者必须要掌握基本的JNI技术,这样才能进行有效的NDK开发。

JNI技术简介

JNI(Java Native Interface)是Java SDK 1.1时正式推出的,目的是为不同JVM实现间提供一个标准接口,从而使Java应用可以使用本地二进制共享库,扩充了原有JVM的能力,同时Java程序仍然无需再次编译就可以运行在其他平台上,即保持了平台独立性又能使用平台相关的本地共享库提升性能。在Java开发中的位置如下图所示。JNI作为连接平台独立的Java层(以下简称Java层)与与平台相关的本地环境(以下简称Native层)之间的桥梁。

/images/role-of-jni-intro.gif

图1-1 JNI在Java开发中的位置

实际上在Android内部就大量的使用了JNI技术,尤其是在Libraries层和Framework层。

何时使用Android NDK

Google在其文档提到了NDK不能让大多数应用获益,其增加的复杂度远大于获得的性能的代价。Google建议当需要做大量的cpu密集同时少量存储操作或者重用C/C++代码时可以考虑使用NDK。 本文的余下部分将具体介绍Android平台下通过NDK的支持的如何进行JNI的开发。

Hello,NDK

本节通过一个简单的例子,介绍NDK开发流程以及JNI的基本使用。 笔者假定你已经下载了NDK,且有Android SDK开发的经验。 在NDK开发包中就有若干的NDK示例。其中 hello-jni 是一个简单的实例。该实例从native层传递字符串到java层,并显示在界面上。(你可以在Eclipse里选择 新建Anroid项目 ,之后选择 “Create project from existing source”,并定位到NDK目录中的Sample/hello-jni ,这样就可以将示例代码导入到Eclipse中。) HelloJni的Java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.example.hellojni;
import android.app.Activity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.os.Bundle;
import android.view.View.OnClickListener;

public class HelloJni extends Activity
{
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Button btn = (Button)findViewById(R.id.btn);
        final TextView txtv = (TextView)findViewById(R.id.txtv);
        btn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                txtv.setText(stringFromJNI());//调用native函数

            }
        });
    }

    /* A native method that is implemented by the
     * "hello-jni" native library, which is packaged
     * with this application.
     * 声明含有native关键词的函数,就可以在类中使用了。
     */
    public native String  stringFromJNI();

    /* 
     * 该函数并没有在共享库中实现,但是仍然可以声明。
     * 没有实现的native函数也可以在类中声明,native方法仅在首次调用时才开始搜索。
     * 若没有找到该方法,会抛出java.lang.UnsatisfiedLinkError异常
     */
    public native String  unimplementedStringFromJNI();

    /* this is used to load the "hello-jni" library on application
     * startup. The library has already been unpacked into
     * /data/data/com.example.HelloJni/lib/libhello-jni.so at
     * installation time by the package manager.
     * 使用静态方式再创建类时就载入共享库,该共享库(后面会介绍)在程序安装后
     * 位于/data/data/com.example.HelloJni/lib/libhello-jni.so
     */
    static {
        System.loadLibrary("hello-jni");
    }
}

Java代码中调用native函数很简单。大致分为以下几步:

  • 调用 System.loadLibrary 方法载入共享库
  • 声明native方法
  • 调用native方法

JNI的使用的一个关键点是 1) 如何找到共享库 2)如何将Java代码中的声明的native方法和实际的C/C++共享库中的代码相关联,即JNI函数注册。 第一个问题可以交给NDK构建工具 ndk-build 解决:通常是将编译好的so共享库放在 libs/armeabi/libXXX.so 之后会有更详细的介绍。第二个问题可以将在第二节中系统讲述,现在我们只简单的说一下如何做。

利用javah生成目标头文件

简易实用的方法是通过利用Java提供的 javah 工具生成和声明的native函数对应的头文件。具体操作是如下:

  1. 命令行进入到你的项目目录中
  2. 确认你的android项目的java代码已经编译,如果存在 bin/ 目录,应该是编译好的。
  3. 确认你的android项目目录中存在 jni 子目录,如果没有则创建一个(我们现在使用的自带的实例代码,因此可以)。
  4. 在项目根目录下执行命令: javah -jni com.example.hellojni.HelloJNI -classpath bin/classes -o jni/hello-jni.h确认javah所在路径已经在的$PATH路径下
  5. 若上一命令执行成功,则会在 jni 目录下生成一个名为 my_jni_header.h 的头文件。

编写C/C++共享库代码

上一步骤我们得到了与Java源文件对应的头文件,因此只要编写 my_jni_header.c ,实现头文件里面的声明的源代码。生成的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_hellojni_HelloJni */

#ifndef _Included_com_example_hellojni_HelloJni
#define _Included_com_example_hellojni_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_hellojni_HelloJni
 * Method:    stringFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI
  (JNIEnv *, jobject);

/*
 * Class:     com_example_hellojni_HelloJni
 * Method:    unimplementedStringFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_unimplementedStringFromJNI
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

可以看到生成的头文件中的函数和示例项目 hello-jni 中的 hello-jni.c 正好对应。据此也可知我们生成的头文件是正确的。 hello-jni.c 源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <string.h>
#include <jni.h>
#include <stdio.h>

/* This is a trivial JNI example where we use a native method
 * to return a new VM String. See the corresponding Java source
 * file located at:
 *
 *   apps/samples/hello-jni/project/src/com/example/HelloJni/HelloJni.java
 */
jstring
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
                                                  jobject thiz )
{
    char msg[100];
    sprintf(msg,"Hello from JNI.");
    return (*env)->NewStringUTF(env, msg);
}

使用NDK提供的工具编译生成共享库

经过以上两步,我们已经得到了C/C++共享库的源代码,现在需要使用交叉编译工具将其编译成目标机器上的二进制共享库。NDK工具提供了一个简单的构建系统,开发者之需要编写 Android.mk ,之后在项目根目录下执行命令 ndk-build 就可以完成交叉编译过程。

1
2
3
4
5
6
7
8
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni2.c

include $(BUILD_SHARED_LIBRARY)

Android.mk 可以看作是小型的makefile,关于 Android.mk 的更多细节,限于篇幅,这里不做详细介绍请参考NDK自带文档,里面有完整的介绍。 输出的信息类似下面:

1
2
3
4
5
Gdbserver      : [arm-linux-androideabi-4.4.3] libs/armeabi/gdbserver
Gdbsetup       : libs/armeabi/gdb.setup
Compile thumb  : hello-jni <= hello-jni.c
SharedLibrary  : libhello-jni.so
Install        : libhello-jni.so => libs/armeabi/libhello-jni.so

上面的信息告诉我们生成好的so文件路径为 libs/armeabi/libhello-jni.so 。至此一个简单的NDK程序的已经制作完成。 总结一下大致过程是:

  • 编写好Java源文件,使用静态代码段载入共享库,并声明native函数。之后编译android项目
  • 使用 javah 工具生成头文件
  • 根据头文件编写native函数
  • 利用 ndk-build 完成共享库的编译

native函数的动态注册方法

上一节我们通过一个简单的实例,对NDK开发有了一个感性的认识。但是你也许会发现Java层上声明的native函数与native上面的实现之间的关联是通过javah生成头文件完成的,这个方法显得很笨拙。 实际上这种静态注册的方法是通过函数名( Java_com_example_hellojni_HelloJni_stringFromJNI )来建立联系。这种做法有诸多弊端:

  • 名字很长,没有可读性。
  • 每个声明了native函数的类都要生成一个对应的头文件,对于真实的应用程序,类文件很多时不现实。
  • 每次载入都需要查询native函数,效率低。

Android内部实现上,在使用JNI时很显然并没有这样做,它采用了更加规范的 动态注册 的方法进行两个层次上的关联。

动态注册版Hello-Jni

以下代码是上面的 hell-jni.c 的动态注册版,代码中使用的是自定义的native函数名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <string.h>
#include <jni.h>
#include <stdio.h>

jstring getHelloString();

static JNINativeMethod gMethods[] = {
    {
    "stringFromJNI",
    "()Ljava/lang/String;",
    (void *)getHelloString
    }
};

static int nMethods = 1;
static JNIEnv *env = NULL;

jstring getHelloString()
{
    char msg[100];
    sprintf(msg,"Hello from JNI.");
    return (*env)->NewStringUTF(env, msg);
}

jint JNI_OnLoad(JavaVM *vm,void *reserved){

    jint result = -1;
    jclass clz = NULL;
    if ((*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4) != JNI_OK){
        return -1;
    }
    clz = (*env)->FindClass(env,"com/example/hellojni/HelloJni");
    if((*env)->RegisterNatives(env,clz,gMethods,nMethods) < 0) {
        return -1;
    }
    return JNI_VERSION_1_4;//根据JNI规范,JNI_OnLoad必须返回版本号常量否则出错。
}

根据Java的官方文档1,当VM载入共享库时,会寻找 jint JNI_OnLoad(JavaVM *vm, void *reserved) 函数,如果存在则再载入共享库之后调用该函数。因此我们可以在该函数中完成native函数的注册工作。 JNI_OnLoad 函数的参数有两个,最主要就是 JavaVM 结构。 JavaVM 是存储VM信息的数据结构。更多信息将在后面讲到,这里我们只需要知道,通过JavaVM指针我们可以得到另一个JNI核心结构—— JNIEnv , JNIEnv 代表了整个JNI环境的数据结构,实际是一个函数表,其中存储了JNI的各种相关操作的函数指针,后文会详细介绍,在这里我们只需要知道在JNIEnv结构有以下的方法,通过调用就可以实现动态注册。

  • jclass FindClass(JNIEnv *env, const char *name) 传入JNIEnv指针和类名称返回代表这个类的结构2
  • jint RegisterNatives(JNIEnv *env, jclass clazz,const JNINativeMethod *methods, jint nMethods)注册native函数的函数3

JNINativeMethod结构

1
2
3
4
5
6
7
8
9
10
11
`RegisterNatives` 用来注册一组native函数,其中使用到了 `JNINativeMethod` 结构,具体定义如下3:

    typedef struct {

        char *name; //Java代码中声明的native函数的名称

        char *signature; //对应Java代码层native函数的签名,下面会介绍

        void *fnPtr; //共享库中函数指针

    } JNINativeMethod;

这里就涉及到了 函数签名

函数签名

Java允许函数重载,因此在注册时就要具体区分出来,否则会出现混乱,因而这里就要使用一种方法将每个Java中的方法标上唯一的标记。这种方法就是 函数签名 。函数签名应该属于JVM内部的规范,不具有可读性。规定4如下:


类型标识 Java类型

Z boolean

B byte

C char

S short

I int

J long

F float

D double

L/java/lang/String; String

[I int[]

[L/java/lang/object; Object[]

V void

表1 类型标示对应表

每个函数签名大致格式 (<参数签名>)返回值类型签名 引用类型的参数签名形式为 L<包名>


Java函数函数签名
声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。