Zane'Blog

JNI和NDK编程-JNI的开发流程

放手一搏吧,别顾虑太多,这就是男人该有的性格

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程序的性能。

JNI和NDK开发所用到的动态库的格式是以.so为后缀的文件,下面统一简称为so库。JNI和NDK主要用于底层和嵌入式开发。

JNI的开发流程

JNI的开发流程有如下几步,首先需要在Java中声明native方法,接着用C或者C++实现native方法,然后就可以编译运行了。

声明native方法

创建一个类,这里叫做JniTest.java,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package io.github.zane.ndkdevdemo;
/**
* Created by Zane on 2017/6/20.
*/
public class JniTest {
static {
System.loadLibrary("jni-test");
}
public static void main(String[] args){
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库的规范。

通过javah命令生成Java所对应的JNI头文件

编译Java源文件得到class文件,然后通过javah命令导出JNI的头文件,具体的命令如下:

javac io/github/zane/ndkdevdemo/JniTest.java

javah io.github.zane.ndkdevdemo.JniTest

这里写图片描述

在当前目录下,会产生一个 io_github_zane_ndkdevdemo_JniTest.h 的头文件:

这里写图片描述

它是javah命令自动生成的,内容如下所示:

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 io_github_zane_ndkdevdemo_JniTest */
#ifndef _Included_io_github_zane_ndkdevdemo_JniTest
#define _Included_io_github_zane_ndkdevdemo_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: io_github_zane_ndkdevdemo_JniTest
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_io_github_zane_ndkdevdemo_JniTest_get
(JNIEnv *, jobject);
/*
* Class: io_github_zane_ndkdevdemo_JniTest
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_io_github_zane_ndkdevdemo_JniTest_set
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif

上面的代码需要做一个说明,首先函数名的格式遵循如下规则:Java_包名_类名_方法名。比如JniTest中的set方法,到这里就变成了 JNICALL Java_io_github_zane_ndkdevdemo_JniTest_set
(JNIEnv *, jobject, jstring) ,其中 io_github_zane_ndkdevdemo 是包名,JniTest是类名,jstring是代表的是set方法的String类型的参数。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++编译时的一些问题,这里就不再展开了。

1
2
3
#ifdef __cplusplus
extern "C" {
#endif

实现JNI方法

JNI方法是指Java中声明的native方法,这里可以选择用C++或者C来实现,它们的实现过程是类似的,只有少量的区别,下面分别用C++和C来实现JNI方法。

1、首先,在工程的主目录下创建一个子目录,名称随意,这里选择“jni”作为子目录的名称。

这里写图片描述

在New Android Component中,直接点击Finish按钮完成JNI目录创建。

这里写图片描述

这里写图片描述

2、然后,将之前通过javah生成的头文件 io_github_zane_ndkdevdemo_JniTest.h 复制到jni目录下。

3、接着在/jni目录下创建 test.cpp 或 test.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//test.cpp
#include "io_github_zane_ndkdevdemo_JniTest.h"
#include <stdio.h>
/*
* Class: io_github_zane_ndkdevdemo_JniTest
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_io_github_zane_ndkdevdemo_JniTest_get (JNIEnv *env, jobject thiz) {
printf("invoke get from C++\n");
return env->NewStringUTF("Hello from JNI !");
}
/*
* Class: io_github_zane_ndkdevdemo_JniTest
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_io_github_zane_ndkdevdemo_JniTest_set (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);
}
//test.c
#include "io_github_zane_ndkdevdemo_JniTest.h"
#include <stdio.h>
/*
* Class: io_github_zane_ndkdevdemo_JniTest
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_io_github_zane_ndkdevdemo_JniTest_get (JNIEnv *env, jobject thiz) {
printf("invoke get from C\n");
return (*env)->NewStringUTF(env, "Hello from Zane !");
}
/*
* Class: io_github_zane_ndkdevdemo_JniTest
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_io_github_zane_ndkdevdemo_JniTest_set (JNIEnv *env, jobject thiz, jstring string) {
printf("invoke set from C\n");
char* str = (char*) (*env)->GetStringUTFChars(env, string, NULL);
printf("%s\n", str);
(*env)->ReleaseStringUTFChars(env, string, str);
}

可以发现,tet.cpp和test.c的实现很类似,但是它们对env的操作方式有所不同,因此用C++和C来实现同一个JNI方法,它们的区别主要集中在对env的操作上,其他都是类似的,如下所示:

1
2
C++:env->NewStringUTF("Hello from JNI !");
C:(*env)->NewStringUTF(env, "Hello from JNI !");

编译so库并在Java中使用

Mac、Linux环境下:

so库的编译这里采用gcc,切换到jni目录中,对于 test.cpp 和 test.c 来说,它们的编译指令如下所示:

1
2
C++:gcc -shared -I /user/tools/jdk/include -fPIC test.cpp -o libjni-test.so
C:gcc -shared -I /user/tools/jdk/include -fPIC test.c -o libjni-test.so

上面的编译命令中,/user/tools/jdk/include是本地的jdk的安装路径,在其他环境编译时将其指向本机的jdk路径即可。而libjni-test.so则是生成的so库的名字,在Java中可以通过如下方式加载:System.loadLibrary(“jni-test”),其中so库名字中的”lib”和”.so”是不需要明确指出的。so库编译完成后,就可以在Java程序中调用so库了,这里通过Java指令来执行Java程序,切换到主目录,执行如下指令:java -Djava.library.path=jni io.github.zane.ndkdevdemo.JniTest,其中-Djava.library.path=jni指明了so库的路径。

Window环境下:

详情请看:NDK的开发流程

坚持原创技术分享,您的支持将鼓励我继续创作!