JNI和NDK编程(一)JNI的开发流程
JNI和NDK编程(二)NDK的开发流程
JNI和NDK编程(三)JNI的数据类型和类型签名
JNI和NDK编程(四)JNI调用Java方法的流程
Java JNI本意为Java Native Interface(java本地接口), 它是为方便java调用C、C++等本地代码所封装的一层接口. 我们都知道,java的有点是跨平台, 但是作为优点的同时, 其在和本地交互的时候就出现了短板. Java的跨平台特性导致其本地交互的能力不够强大, 一些和操作系统相关的特性Java无法完成, 于是Java提供了JNI专门用于和本地代码交互, 这样就增强了Java语言的本地交互能力. 通过Java JNI, 用户可以调用C、C++所编写的本地代码.
NDK是android所提供的一个工具集合, 通过NDK可以在Android中更加方便地通过JNI来访问本地代码, 比如C或者C++. NDK还提供了交叉编译器, 开发人员只需要简单的修改mk文件就可以生成特定的CPU平台的动态库. 使用NDK有好处如下:
在Linux环境中, JNI和NDK开发所用到的动态库的格式是以.so为后缀的文件, 下面统称为so库. 另外, 由于JNI和NDK主要用于底层和嵌入式开发, 在Android的应用层开发中使用较少, 加上它们本身更加侧重于C和C++方面的编程, 本篇只介绍JNI和NDK的基础知识, 其他更加深入的知识点可以查看专门介绍JNI和NDk的书籍.
JNI开发流程有如下几步, 首先需要在java中声明native方法, 接着用c/c++实现native的方法, 然后就可以编译运行了.
1. 在Java中声明native方法
创建一个类, 这里叫做JniTest.java, 代码如下所示.
public class JniTest {
static {
System.loadLibrary("jni-test");
}
public static void main(String arg[]) {
JniTest jniTest = new JniTest();
System.out.println(jniTest.get());
jniTest.set("hello world");
}
public native String get();
public native void set(String str);
}
可以看到上面的代码中, 生命了两个native方法: get和set(String), 这来两个就是需要在JNI中实现的方法. 在JniTest的头部有一个加载动态库的过程, 其中jni-test是so库的标识, so 库完整的名称为 libjni-test.so , 这是加载so库的规范.
2. 编译Java源文件得到class文件, 然后通过javah命令导出JNI头文件
javac com/gavinandre/jnitest/JniTest.java
javah com.gavinandre.jnitest.JniTest
在当前目录下, 会产生一个com_gavinandre_jnitest_JniTest.h头文件, 它是javah命令自动生成的,内容如下所示.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_gavinandre_jnitest_JniTest */
#ifndef _Included_com_gavinandre_jnitest_JniTest
#define _Included_com_gavinandre_jnitest_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_gavinandre_jnitest_JniTest
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_gavinandre_jnitest_JniTest_get
(JNIEnv *, jobject);
/*
* Class: com_gavinandre_jnitest_JniTest
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_gavinandre_jnitest_JniTest_set
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
上面的代码需要做一下说明, 首先函数名的格式遵循如下规则: Java_包名_类名_方法名. 比如JniTest中的set方法, 到这里就变成了JNIEXPORT void JNICALL Java_com_gavinandre_jnitest_JniTest_set(JNIEnv *, jobject, jstring), 其中com_gavinandre_jnitest是包名, JniTest是类名, **jstring 代表的是set方法的String类型参数: set(String str) **. 关于Java和JNI的数据类型之间的对应关系会在下文 JNI的数据类型和类型签名 中介绍, 这里只需要知道Java的String对应与JNI的jstring即可. JNIEXPORT、JNICALL、JNIEnv和jobject都是JNI标准中所定义的类型或者宏,它们的含义如下:
下面这个宏定义是必须的, 作用是指定extern "C"内部的函数采用C语言的命名风格来编译. 否则当JNI采用C++来实现时, 由于C/C++编译过程对函数的命名风格不同, 这将导致JNI在链接时无法根据函数名找到具体的函数, 那么JNI调用就无法完成. 更多的细节实际上是有关C和C++编译时的一些问题, 这里就不在展开了.
#ifdef __cplusplus
extern "C" {
#endif
3. 实现JNI方法
JNI方法是指的Java中声明的native方法, 这里可以选择c++和c来实现. 过程都是类似的. 只有少量的区别, 下面分别用C++和C来实现JNI方法.
首先是C++, 在工程的主目录创建一个子目录, 名称任意, 这里选择jni作为子目录的名称, 然后将之前通过javah命令生成的.h头文件复制到创建的目录下, 接着创建test.cpp实现如下:
// test.cpp
#include "com_gavinandre_jnitest_JniTest.h"
#include
JNIEXPORT jstring JNICALL Java_com_gavinandre_jnitest_JniTest_get(JNIEnv *env, jobject thiz){
printf("invoke get in C++\n");
return env->NewStringUTF("Hello from JNI !");
}
JNIEXPORT void JNICALL Java_com_gavinandre_jnitest_JniTest_get(JNIEnv *env, jobject thiz, jstring string){
printf("invoke set from C++\n");
char* str = (char*) env->GetStringUTFChars(string, NULL);
printf("%s\n, str");
env->ReleaseStringUTFChars(string, str);
}
然后是c,创建test.c实现如下:
// test.c
#include "com_gavinandre_jnitest_JniTest.h"
#include
JNIEXPORT jstring JNICALL Java_com_gavinandre_jnitest_JniTest_get(JNIEnv *env, jobject thiz){
printf("invoke get in C\n");
return (*env)->NewStringUTF(env, "Hello from JNI .");
}
JNIEXPORT void JNICALL Java_com_gavinandre_jnitest_JniTest_set(JNIEnv *env, jobject thiz, jstring string){
printf("invoke get from C\n");
char* str = (char*) (*env)->GetStringUTFChars(env, string, NULL);
printf("%s\n, str");
(*env)->ReleaseStringUTFChars(env, string, str);
}
其实C\C++在实现上很相似, 但是对于env的操作方式有所不同, 因此用C++和C来实现同一个JNI方法, 它们的区别主要集中来对env的操作上, 其他都是类似的, 如下所示.
C++: env->NewStringUTF("Hello from JNI !");
C: (*env)->NewStringUTF("Hello from JNI .");
4. 编写so库并在java中调用
so库的编译这里采用gcc, 切换到jni目录中, 对于test.cpp和test.c来说, 它们的编译指令如下所示.
gcc -shared -I /usr/lib/jvm/java-8-oracle/include -fPIC test.cpp(test.c) -o libjni-test.so
上面的编译命令中, /usr/lib/jvm/java-8-oracle是本地jdk的安装路径, 在其他环境编译时将其指向本机的jdk路径即可. 而libjni-test.so则是生成的so库的名字, 在Java中可以通过如下方式加载: System.loadLibrary(“jni-test”), 其中so库名字中的"lib"和".so"是不需要明确指出的. so库编译完成后, 就可以在Java程序中调用so库了, 这里通过Java执行来执行Java程序, 切换到主目录, 执行如下指令: java -Djava.library.path=jni com.gavinandre.jnitest.JniTest , 其中-Djava.library.path=jni指明了so库的路径.
首先采用C++生成so库,程序运行后日志如下:
invoke get in C++
Hello from JNI !
invode set from C++
hello world
然后采用C生成so库,程序运行后日志如下:
invoke get in C
Hello from JNI !
invode set from C
hello world
通过上面的日志可以发现, 在Java中成功的调用C/C++的代码,这就是JNI典型的工作流程.