NDK(Native Development Kit)开发是让开发者能够在 Android 平台上使用 C/C++ 语言进行编程的工具集,它是一项强大而又具有挑战性的技术。
NDK 开发的作用
- 性能优化:对于一些计算密集型或对性能要求苛刻的任务,比如图像处理,音频处理,物理模拟等,使用 C/C++ 编写可以充分发挥底层硬件的性能,运行效率更高。
- 代码复用:如果已经有现有的 C/C++ 代码库,例如一些成熟的算法库,游戏引擎等,可以通过 NDK 将其集成到 Android 应用中,避免重新开发。
- 加密保护:某些关键的加密算法或安全相关的代码,使用 C/C++ 编写相对更难被反编译和破解,增加代码的保密性和安全性。在 Java/Kotlin 代码中,由于其字节码的特性,反编译相对容易,而 C/C++ 编译后的二进制代码更难被直接理解和逆向工程。
- 跨平台开发:如果需要同时支持多个平台,将核心逻辑用 C/C++ 编写,然后通过 NDK 集成到 Android 中,可以减少在不同平台上的重复开发工作。
- 深入系统交互:某些情况下,需要直接与底层系统进行交互,比如访问特定的硬件设备,实现底层网络协议等,NDK 可以提供这种能力。
NDK 与 JNI 的关系
JNI 的全称为 Java Native Interface,即 Java 的本地接口,可以实现 Java 与 C/C++ 语言的交互。NDK 和 JNI 就像是一对好搭档,NDK 就像是一个装满各种工具和资源的大箱子,能让我们用 C/C++ 语言来编写代码,然后把这些代码整合到 Android 应用中,而 JNI 呢,更像是一座连接 Java 世界和 C/C++ 语言世界的桥梁,它让 Java 代码能够和用 C/C++ 写的代码相互交流,传递数据。没有 JNI,Java 代码和 C/C++ 代码就像是两个语言不通的小伙伴,没法一起愉快地玩耍。
简单来说,NDK 提供了开发的环境和工具,而 JNI 负责在 Java 和原生代码之间牵线搭桥,让它们能够协同工作。
创建 Native C++ 项目
创建 NDK Project 之前,我们得先确保本地有 NDK 环境。如果没有,则需要先下载并配置好环境变量。
环境变量配置好之后,用 Android Studio 创建一个 Native C++ Project
Finish 之后,Android Studio 会为我们创建一些相关的代码文件,我们先来看看 build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 33
defaultConfig {
applicationId "com.jian.nativelib"
minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 用于配置 cmake 命令参数
externalNativeBuild {
cmake {
cppFlags ''
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
// 定义 cmake 版本和构建脚本 CMakeLists 的路径
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.18.1'
}
}
buildFeatures {
viewBinding true
}
ndkVersion '25.1.8937393'
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
build 文件里会有 externalNativeBuild 配置项,我们再看下官方提供的简单 JNI 交互示例,Android Studio 会为我们创建如下两个文件。
CMakeLists 是 cmake 的构建脚本,具体看注释,如下所示:
# 设置 cmake 最小版本
cmake_minimum_required(VERSION 3.18.1)
# 声明和命名项目
project("nativelib")
# 编译 library
add_library( # 设置 library 名称
nativelib
# 设置 library 模式,SHARED 会编译 so 文件,STATIC 则不会
SHARED
# 设置原生代码路径
native-lib.cpp)
# 搜索 library
find_library( # 设置路径变量的名称
log-lib
# 指定希望 cmake 查找的 NDK 库的名称
log)
# 关联 library
target_link_libraries( # 指定目标库
nativelib
# 将目标库链接到 NDK 中包含的日志库
${log-lib})
native-lib 就是需要实现的原生代码,方法名的格式是:Java_包名_类名_方法名
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_jian_nativelib_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
JNIEnv 是指向 JNI 环境的指针,可以用它来访问 JNI 提供的接口方法,jobject 表示对象中的 this。
对应的 MainActivity 调用代码如下:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.sampleText.text = stringFromJNI()
}
// external 用于声明某个方法不由 Kotlin 实现,与 Java 的 native 相似
external fun stringFromJNI(): String
companion object {
init {
System.loadLibrary("nativelib")
}
}
}
对应的数据类型
两边对应的数据类型,Android Studio 会帮我们生成,我们只需定义好方法,比如,我们 MainActivity 中添加一个方法。
点击 Create JNI function for add,就会自动生成对应的数据类型代码。
extern "C"
JNIEXPORT jint JNICALL
Java_com_jian_nativelib_MainActivity_add(JNIEnv *env, jobject thiz, jint x, jint y) {
// TODO: implement add()
}
再比如,在 MainActivity 中添加一个带引用类型的方法。
external fun getArraySize(a: IntArray, b: FloatArray): Int
生成对应的数据类型如下,其实大多就是在类型前面加个 “j”。
extern "C"
JNIEXPORT jint JNICALL
Java_com_jian_nativelib_MainActivity_getArraySize(JNIEnv *env, jobject thiz, jintArray a,
jfloatArray b) {
// TODO: implement getArraySize()
}
处理对象
JNI 会把所有对象当作一个 C 指针传递到本地方法中,这个指针指向 JVM 内部数据结构,而内部数据结构在内存中的存储方式是不可见的,所以只能通过 JNIEnv 指针指向的函数来操作 JVM 的数据结构。
比如,我们有一个方法,操作一个字符串。
external fun handleString(str: String): String
由于字符串属于引用类型,所以不能像访问基本数据类型那样使用,native 只能通过 JNI 函数来访问字符串内容,代码如下:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jian_nativelib_MainActivity_handleString(JNIEnv *env, jobject thiz, jstring str) {
// 从内存中拷贝出来供 native 使用
const char *copyStr = (char *) env->GetStringUTFChars(str, NULL);
if (copyStr == NULL) {
return NULL;
}
char newStr[128] = {0};
strcpy(newStr, copyStr);
// 附加字符串
strcat(newStr, "NDK");
// 需要手动释放资源
env->ReleaseStringUTFChars(str, copyStr);
// 编码转换,因为 C/C++ 默认使用 UTF 编码,而 Kotlin 是 Unicode 编码。
return env->NewStringUTF(newStr);
}
对于基本数据类型的数组,可以直接访问,举个例子,有个处理数组的方法,如下所示:
external fun handleArray(a: IntArray)
对应的 native 方法为
extern "C"
JNIEXPORT void JNICALL
Java_com_jian_nativelib_MainActivity_handleArray(JNIEnv *env, jobject thiz, jintArray a) {
// 获取数组长度
jint length = env->GetArrayLength(a);
// 转化成本地数组
jint *array = env->GetIntArrayElements(a, nullptr);
// 遍历改变数组的值
for (int i = 0; i < length; i++) {
array[i] = i;
}
// 更新并释放数组
env->ReleaseIntArrayElements(a, array, 0);
}
而数组元素为对象时,则不能直接访问,比如需要处理一个字符串数组
external fun handleObjArray(strArray: Array<String>)
extern "C"
JNIEXPORT void JNICALL
Java_com_jian_nativelib_MainActivity_handleObjArray(JNIEnv *env, jobject thiz,
jobjectArray str_array) {
// 获取数组长度
jint length = env->GetArrayLength(str_array);
for (int i = 0; i < length; i++) {
// 获取元素
jstring element = (jstring) env->GetObjectArrayElement(str_array, i);
// 设置元素
env->SetObjectArrayElement(str_array, i, env->NewStringUTF("NDK"));
}
}
so 库
为了方便使用,NDK 提供了一些脚本,使得更容易编译 C/C++ 代码,这个编译文件为 so 文件,就是 C/C++ 库。so 文件在程序运行时就会加载,要调用 so 文件,必有某个 kotlin 类运行时加载 native 库,并通过 JNI 调用了它的方法,所以一般情况下,都是 jar 包和 so 库一起提供出去,下面就简单整一个吧!
我们新建一个 NDK 项目,然后再创建一个 Module,在这个 Module 中创建一个类,用于调用 JNI 方法。
class JniMethod {
companion object {
init {
System.loadLibrary("jniso")
}
external fun stringFromJNI(): String
}
}
对应的 native 方法为
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jian_mylibrary_JniMethod_00024Companion_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
然后我们运行这个项目,然后 so 库在这里。
默认支持 x86,如果想支持多种库架构,则可在 build.gradle 中配置一下,配置后记得 Clean 和同步一下。
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags ''
}
}
ndk {
// 设置支持的 so 库架构
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
对应的调用方法在 kotlin 文件中,所以,我们需要将这个 module 打包成 jar 包。执行命令
然后就会在该路径下生成 jar 包
现在 so 库和 jar 包都有了,就可以提供出去啦,将 so 库放到 jniLibs 目录下,jar 包放在 libs 目录下,然后项目中引入就行啦 ~