JNI与NDK入门之一

概述

Android 系统架构中包含了 Applications (应用程序层)、Application framework(应用程序框架层)、Libraries + Android runtime(系统运行库层)以及 Linux Kernel(Linux核心层)。从编程语言的角度看,每层的功能模块都是使用相应的语言编写的,在此过程中,C/C++ 与 Java相互通信时就需要一个媒介来联系起来,JNI(Java Native Interface) 就充当了这一角色,它允许 Java 代码和 基于 C/C++ 编写的应用程序、模块、库进行交互操作。

JNI 原理

使用情形

通常在下列几种情形下使用 JNI:

  • 追求运行速度

    不管是 Dalvik 还是 Art 虚拟机上,Java 代码的运行速度在一些情况下还是无法媲美使用 C/C++ 来开发的应用程序,特别是在开发图形处理或信号处理这类对 CPU 处理速度有较高要求的程序。我们可以使用 C/C++ 这类本地语言开发,再在 Java 中 借助JNI 将 Java 程序与 C/C++ 模块连接在一起,从而开发出一个执行效率更高的程序。

  • 复用 C/C++代码

    由于历史积累的问题,我们可能需要重新使用一些已经编写好且通过测试的 C/C++代码,减少了重复工作又确保了程序的安全性与健壮性。

  • 控制硬件

    硬件控制代码或者设备驱动程序通常使用 C 语言编写,借助 JNI 将设备驱动程序映射为 Java API,就可以在 Java 层实现对硬件的控制。

基本原理

在 Java 中调用 C 库函数,流程基本分为以下六个:

  1. 编写 Java 代码
  2. 编译 Java 代码
  3. 生成 C 语言头文件
  4. 编写 C 代码
  5. 生成 C 共享库
  6. 运行 Java 程序

下面我们来通过编写一个简单的 Java 程序来演示如何通过 JNI 来调用 C 函数。被调用的 C 函数是一个向控制台输出字符串的简单函数。

编写 Java 代码

首先编写 Java 端的调用代码,其中声明本地方法,而方法的具体实现则通过之后的 C/C++来编写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public class HelloJNI {
// 使用 native 关键字声明本地方法,该方法与用 C/C++ 编写的 JNI
// 函数相对应, Java 代码中只声明没有具体实现
native void printHello();
native void printStr(String content);

// 使用静态块加载具体实现本地方法的 C 运行库
static {
System.loadLibrary("hellojni");
}

public static void main(String args[]) {
HelloJNI jni = new HelloJNI();

// 调用本地方法(实际调用的是使用 C 语言编写的 JNI 本地函数)
jni.printHello();
jni.printStr("The content from Java to C");
}

}

编译 Java 代码

使用 Java 编译器 (javac)编译 Java 源代码:

1
javac HelloJNI.java

此时直接运行 HelloJNI 会抛出
java.lang.UnsatisfiedLinkError 的异常。

JNI 初步运行

生成 C 语言头文件

下面我们生成数原型将运行库中的 C 函数与Java 代码中的本地方法映射在一起。
函数原型存在于 C/C++ 的头文件中。我们使用 javah 来生成头文件:

1
javah <包含以 native 关键字声明的方法的 Java 类名称>

JNI 生成头文件

运行 javah 命令后,会在当前目录下生成与 Java 类名(即 javah 命令的参数)相同名称的 C 语言头文件。在这个 C 头文件中,定义了与 Java 本地方法相链接的 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
30
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: printHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_printHello
(JNIEnv *, jobject);

/*
* Class: HelloJNI
* Method: printStr
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_HelloJNI_printStr
(JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

上述文件内容中, JNIEXPORTJNICALL 都是 JNI 关键字,表示此函数要被 JNI 调用。反过来说, JNI 如果要正常调用函数,那么函数原型中必须要有这两个关键字。其实,这两个够关键字都是宏定义,具体请查看 JDK/include 下的 jni_md.h 文件。

同时可以看到生成的函数原型名称遵循「Java_类名_本地方法名」的命名规则,即可推断出 JNI 本地函数与 Java 类的哪个本地方法相对应。

现在查看函数原型中的参数,带有两个默认参数,第一个、第二个分别为 JNIEnv *jobject,前者是 JNI 接口的指针,用来调用 JNI 表中的各类 JNI 函数。后者是 JNI 提供的 Java 本地类型,保存着调用本地方法的对象的一个引用,用来在 C 代码中访问 Java 对象。

编写 C/C++ 代码

在 C 函数原型生成后,我们编写 hellojni.c 文件来具体实现 JNI 本地函数。首先把定义在 HelloJNI.h 头文件中的函数原型复制到 hellojni.c 文件中。

生成 C 共享库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

#include "HelloJNI.h"
#include <stdio.h>

// 从头文件中复制后,记得添加参数名
JNIEXPORT void JNICALL Java_HelloJNI_printHello
(JNIEnv *env, jobject obj) {
printf("Hello World!\n");
}


JNIEXPORT void JNICALL Java_HelloJNI_printStr
(JNIEnv *env, jobject obj, jstring string) {
// C 语言中字符串占用8位, Java 中字符串占用16位,因此
// 需要将 jstring 类型的字符串转换成 C 语言字符串
const char *str = (*env)->GetStringUTFChars(env, string, 0);
printf("%s!\n", str);
}

在命令行中,输入如下编译命令:

1
2
3
# 本机是 mac 环境,生成的是动态链接库是 .dylib 作为扩展名( Mach-O 格式),linux 改成 .so,windows 下则是 .dll。
# 至于为什么 unix/linux 下的动态链接库命名规则需要加上 .lib 的原因请参考 http://www.swig.org/Doc1.3/Java.html#dynamic_linking_problems
gcc hellojni.c -I /Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/include -I /Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/include/darwin -shared -o libhellojni.dylib

注意这里几个 gcc 的参数,-shared 说明要生成动态库,对于两个 -I 的选项,前者指定了头文件 <jni.h> 的存放路径,后者指定了头文件 <jni_md.h> 的存放路径,请根据你的环境配置更改。

JNI 生成动态链接库

运行 Java 程序

到这里所有步骤准备完成,执行 java 命令,运行 HelloJNI 类后,查看运行结果。
请注意 java.library.path 用来指定当前动态链接库地址。

1
java -Djava.library.path=. HelloJNI

JNI 调用成功

小结

最后,我们总结一下 Java 本地方法如何通过 JNI 链接至 C 函数的几个步骤:

JNI 流程

  1. 在 Java 类中声明本地方法
  2. 使用 javah 命令,生成包含 JNI 本地函数原型的头文件
  3. 实现 JNI 本地函数
  4. 生成 C 共享库
  5. 通过 JNI 调用 JNI 本地函数