您的位置:首页 > 编程语言

JNI和NDK编程(一)JNI的开发流程

2017-04-03 15:15 337 查看
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有好处如下:

提高代码的安全性. 由于so库反编译比较困难, 因此NDK提高了Android程序的安全性.

可以很方便地使用目前已有的C/C++开源库.

便于平台间的移植. 通过C/C++实现的动态库可以很方便地在其他平台上使用.

提高程序在某些特定情形下得执行效率, 但是不能明显提升Android程序的的性能.

在Linux环境中, JNI和NDK开发所用到的动态库的格式是以.so为后缀的文件, 下面统称为so库. 另外, 由于JNI和NDK主要用于底层和嵌入式开发, 在Android的应用层开发中使用较少, 加上它们本身更加侧重于C和C++方面的编程, 本篇只介绍JNI和NDK的基础知识, 其他更加深入的知识点可以查看专门介绍JNI和NDk的书籍.

JNI的开发流程

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 <jni.h>
/* 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标准中所定义的类型或者宏,它们的含义如下:

JNIEnv *: 表示一个指向JNI环境的指针, 可以通过它来访问JNI提供的接口方法.

jobject: 表示java对象中的this.

JNIEXPORT和JNICALL: 它们是JNI种所定义的宏, 可以在jni.h这个头文件中查到

下面这个宏定义是必须的, 作用是指定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 <stdio.h>
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 <stdio.h>
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典型的工作流程.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java jni