JNI与NDK入门之一
概述
Android 系统架构中包含了 Applications (应用程序层)、Application framework(应用程序框架层)、Libraries + Android runtime(系统运行库层)以及 Linux Kernel(Linux核心层)。从编程语言的角度看,每层的功能模块都是使用相应的语言编写的,在此过程中,C/C++ 与 Java相互通信时就需要一个媒介来联系起来,JNI(Java Native Interface) 就充当了这一角色,它允许 Java 代码和 基于 C/C++ 编写的应用程序、模块、库进行交互操作。
使用情形
通常在下列几种情形下使用 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 库函数,流程基本分为以下六个:
- 编写 Java 代码
- 编译 Java 代码
- 生成 C 语言头文件
- 编写 C 代码
- 生成 C 共享库
- 运行 Java 程序
下面我们来通过编写一个简单的 Java 程序来演示如何通过 JNI 来调用 C 函数。被调用的 C 函数是一个向控制台输出字符串的简单函数。
编写 Java 代码
首先编写 Java 端的调用代码,其中声明本地方法,而方法的具体实现则通过之后的 C/C++来编写。
1 |
|
编译 Java 代码
使用 Java 编译器 (javac)编译 Java 源代码:
1 | javac HelloJNI.java |
此时直接运行 HelloJNI 会抛出java.lang.UnsatisfiedLinkError
的异常。
生成 C 语言头文件
下面我们生成数原型将运行库中的 C 函数与Java 代码中的本地方法映射在一起。
函数原型存在于 C/C++ 的头文件中。我们使用 javah
来生成头文件:
1 | javah <包含以 native 关键字声明的方法的 Java 类名称> |
运行 javah
命令后,会在当前目录下生成与 Java 类名(即 javah 命令的参数)相同名称的 C 语言头文件。在这个 C 头文件中,定义了与 Java 本地方法相链接的 C 函数原型。
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
上述文件内容中, JNIEXPORT
、JNICALL
都是 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 |
|
在命令行中,输入如下编译命令:
1 | 本机是 mac 环境,生成的是动态链接库是 .dylib 作为扩展名( Mach-O 格式),linux 改成 .so,windows 下则是 .dll。 |
注意这里几个 gcc
的参数,-shared
说明要生成动态库,对于两个 -I
的选项,前者指定了头文件 <jni.h>
的存放路径,后者指定了头文件 <jni_md.h>
的存放路径,请根据你的环境配置更改。
运行 Java 程序
到这里所有步骤准备完成,执行 java
命令,运行 HelloJNI
类后,查看运行结果。
请注意 java.library.path
用来指定当前动态链接库地址。
1 | java -Djava.library.path=. HelloJNI |
小结
最后,我们总结一下 Java 本地方法如何通过 JNI 链接至 C 函数的几个步骤:
- 在 Java 类中声明本地方法
- 使用 javah 命令,生成包含 JNI 本地函数原型的头文件
- 实现 JNI 本地函数
- 生成 C 共享库
- 通过 JNI 调用 JNI 本地函数