一、JNI 简介
JNI (Java Native Interface,Java本地接口)是一种编程框架,使得Java虚拟机中的Java程序可以调用本地应用/或库,也可以被其他程序调用。 本地程序一般是用其它语言(C、C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序。
关联文章:
要编写 JNI 就得需要集成 JNI的开发环境(NDK)。接下来我们看一下如何编写Android JNI ,以及需要的流程。
二、NDK是什么
NDK(Native Development Kit缩写)一种基于原生程序接口的软件开发工具包,可以让您在 Android 应用中利用 C 和 C++ 代码的工具。通过此工具开发的程序直接在本地运行,而不是虚拟机。
在Android中,NDK是一系列工具的集合,主要用于扩展Android SDK。NDK提供了一系列的工具可以帮助开发者快速的开发C或C++的动态库,并能自动将so和Java应用一起打包成Apk。同时,NDK还集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so文件。
三、NDK配置
Step1: NDK 相关工具的下载。
Step2: 配置好后,新建一个 Native 工程。
Step3: 选择C++的版本。
到这里,一个 Native 的工程就已经创建完成了。下面我们看下这个 NDK 工程的相关信息。
四、NDK 工程目录
4.1 目录结构
整体工程目录结构如下图所示,与普通的 Android 工程目录相比,Native工程目录中多了一个 cpp 目录,以及目录内对应的 CMakeList.txt
文件。
4.2 build.gradle
除了 cpp 文件夹的区别外,我们再来看下 build.gradle 的区别。
可以看到,相比普通的 Android 应用,build.gradle 配置中多了两个 externalNativeBuild
配置项。
- defaultConfig里面的的 externalNativeBuild 主要是用于配置Cmake的命令参数。
- 外部的 externalNativeBuild 的主要是定义了 CMake 的构建脚本 CMakeLists.txt 的路径。
4.3 CMakeLists文件
接下来我们来看一下 CMakeLists.txt 文件,CMakeLists.txt 是 CMake 的构建脚本,作用相当于 ndk-build 中的 Android.mk,代码如下。
# 设置CMake最小版本
cmake_minimum_required(VERSION 3.18.1)
# 设置项目名称
project("jnitestdemo")
# 编译library,生成链接库jnitestdemo。如果当前这个链接库与其它库有关联,可以使用target_link_libraries进行关联。
add_library(
# 设置library名称
jnitestdemo
# 设置library模式
# SHARED模式会编译so文件,STATIC模式不会编译
SHARED
# 设置原生代码路径
native-lib.cpp)
# 查找log的路径,并赋值给log-lib这个变量。
find_library(log-lib log)
# 将两个链接库关联到一起。
target_link_libraries(
# 指定要关联其它lib的库,这里指当前的jnitestdemo。
jnitestdemo
# 将log-lib关联到jnitestdemo上(相当于是jnitestdemo依赖log-lib)。
${log-lib})
关于 CMake 部分的相关内容,可以查看 CMake 教程。
五、工程实践
上面介绍了工程的配置部分,下面我们来具体看下项目中的代码。
JNI开发流程
- 编写 Java 类,声明了Native方法。
- 编写 Native 代码。
- 将 Native 代码编译成so文件。
- 在 Java 类中引入so库,调用 Native 方法。
其中第1,4步在Java层,第2步在Native层,第3步属于打包编译流程。
Java 层:
下面演示的是 Java 代码调用 Native 代码获取一个字符串的流程。
package com.elson.jnitestdemo;
public class MainActivity extends AppCompatActivity {
static {
// 1.加载动态链接库
System.loadLibrary("jnitestdemo");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.sample_text);
// 2.通过JNI从Native层获取数据。
textView.setText(stringFromJNI());
}
// 3.定义一个Native函数。
public native String stringFromJNI();
}
Java层主要做了3件事。
- 加载动态链接库 jnitestdemo,这个库的名字在 CMakeLists.txt 文件中的 add_library() 中声明指定。
- 通过 JNI 从 Native 层获取 String 类型的数据。
- 定义一个返回String类型的 Native 函数给步骤2中使用。
Native 层:
下面看下 Native 层的代码实现。
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_elson_jnitestdemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
小结:
- JNIEXPORT 和 JNICALL 是JNI中所定义的宏,所以需要引入 <jni.h> 这个头文件。
- 因为方法返回了一个 String 类型的数据,所以引入 C++ 的 <sting> 库。
- extern “C” 的作用:指示编译器按C语言(而不是C++)的方式进行编译。
- C编译:C语言的方法签名只包含方法名,不包含参数,所以C语言中不存在方法重载。
- C++编译: C++的方法签名包含方法名和含参数,所以存在方法重载。
- jobect对象:jobject 表示Java对象中的this,如果是静态方法则表示jclass。
- JNIEnv对象:代表Java调用Native层的环境,一个封装了几乎所有的JNI方法的指针。其只在创建它的线程有效,不能跨线程传递,不同的线程的JNIEnv彼此独立。
到这里,一个简单的 Native 工程就已经构建完成了,也完成了 Java 对 Native 方法的调用。