NDK/JNI的简介与实战
前言
在前面的一系列文章中,我们回顾到了C/C++的语法,以及C/C++的编译,以及CMake的相关语法和实战应用。
接下来我们关注于 Android 平台下的 C/C++ 的应用,因为 Android 平台以其开放性和灵活性而广受欢迎。随着技术的不断进步,对于性能和效果的要求也愈发提高,特别是在图像处理、游戏开发和复杂算法实现等场景中,Java 的执行效率有时不能满足需求,因此引入 C/C++ 语言以利用其高性能和底层操作的能力变得尤为重要。
为此,Android 提供了 NDK(Native Development Kit),允许开发者在应用中使用 C/C++ 代码,这样不仅能实现性能优化,还能重用已有的 C/C++ 代码库。NDK 的使用使得 Android 应用能够在 CPU 密集型任务中获得显著的性能提升。
然而,NDK 的使用涉及到 C/C++ 的编译、链接以及与 Java/Kotlin 代码的交互,这就需要我们了解 JNI(Java Native Interface)。JNI 是一个强大的工具,它提供了一种桥梁,使 Java 代码能够与 C/C++ 代码进行相互调用,这为开发者在 Android 应用中实现高性能和复杂功能提供了可能。
在接下来的文章中,我们将深入探讨 NDK 和 JNI 的相关知识,从基础概念到实际项目开发,带领大家逐步掌握如何在 Android 项目中有效地使用 NDK 和 JNI,以满足日益增长的性能需求和业务需求。
一、NDK的简介
NDK(Native Development Kit) 是Android官方提供的原生代码开发套件,她可以做哪些事情?
- 复用C/C++代码:直接集成现有C/C++库(如OpenCV、FFmpeg)。
- 突破性能瓶颈,更能发挥硬件极限性能。
- 增强安全性,通过编译后的二进制文件保护核心算法,反编译难度高于Java字节码。

NDK 主要就是聚焦底层原生代码编译(.so/.a文件生成)与跨语言交互支持。

一般来说 JNI 是一大块内容,这个单独章节来讲,关于 NDK 的编译主流也是分为 ndk-build/CMake 两种。
-
ndk-build 是 NDK 提供的一种传统的构建系统,基于 Android.mk 和 Application.mk 文件。这套构建系统较早期使用,适合小型或中等规模的项目。
-
CMake 是一个跨平台的构建系统,越来越多地被 Android 开发者使用。CMake 支持更复杂的项目结构,自带丰富的功能,例如支持多种编译器和平台。
关于 CMake 结合 NDK 的编译我准备单独开一篇讲,本文先讲一下 ndk-build 的方式
二、使用 ndk-build 的示例
之前我们讲 CMkae 的时候在 Android Studio 中配置的是 cpp 的目录加上 cmakelist.txt 的配置文件。
其实同样的方式我们可以使用 ndk-build 的方式,此时我们可以使用 jni 的目录加上 Android.mk 的配置文件。

使用 nkd-build 的方式与 CMake 的方式除了文件夹与配置文件不同,在 build.gradle 中的配置也不同:
ini
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
}
android {
namespace = "com.example.testnative"
compileSdk = 34
defaultConfig {
applicationId = "com.example.testnative"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = 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"
}
//配置外部原生构建
externalNativeBuild {
ndkBuild {
path = file("src/main/jni/Android.mk")
}
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
其他的代码都是一样的,比如 native-lib.cpp:
c
#include <jni.h>
#include <string>
#include <android/log.h>
#include "print.h"
const char * LOG_TGA = "LOG_TGA"; //定义日志的TAG
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_testnative_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
//输出debug级别的日志信息
__android_log_print(ANDROID_LOG_DEBUG, LOG_TGA, "hello form native log");
std::string combinedStr = std::string("测试Print库的信息 - ") + hello;
std::string logStr = log(combinedStr);
return env->NewStringUTF(logStr.c_str());
}
和之前是一样的逻辑,并且 print 方法也是之前的逻辑,格式化 LOG 加上日期的工具。
主要的区别就是配置文件 Android.mk :
makefile
# 当前目录是 jni/
LOCAL_PATH := $(call my-dir)
# ========== 1. 定义 print 的头文件和源文件路径 ==========
PRINT_INCLUDE_DIR := $(LOCAL_PATH)/print/include # print.h 所在目录
PRINT_SRC_FILE := $(LOCAL_PATH)/print/src/print.cpp # print.cpp 所在路径
include $(CLEAR_VARS)
# ========== 2. 配置 native-lib 模块 ==========
# 生成的库名:libnative-lib.so
LOCAL_MODULE := native-lib
# 源文件列表:包含主代码 + print 实现
LOCAL_SRC_FILES := native-lib.cpp \
$(PRINT_SRC_FILE)
# 头文件搜索路径:让编译器找到 print.h
LOCAL_C_INCLUDES += $(PRINT_INCLUDE_DIR)
# 链接 Android 日志库
LOCAL_LDLIBS := -llog
# 使用 C++ 标准库(根据 Application.mk 中的 APP_STL)
LOCAL_CPP_FEATURES := exceptions rtti
LOCAL_CPPFLAGS := -std=c++17
include $(BUILD_SHARED_LIBRARY)
这里加上了详细的注释,然后运行就可以得到对应的效果了:

可以看到如果是小项目,其实 ndk-build 的方式配置起来会更加简单。(但我还是用CMake)
三、JNI的介绍
从上面的示例中 native-lib.cpp 我们就可以看到它并不是标准的 C/C++ 语法,熟悉的同学都知道了这是用到了 JNI。
JNI 允许java虚拟机(VM)内运行的java代码与C++、C++和汇编等其他编程语言编写的应用程序和库进行互操作。
在一些特定的场景下如实时渲染、音视频处理、游戏引擎等,我们会选择用 C/C++ 实现,再通过 JNI 调用,因为 Java 运行效率不及 C/C++。
如一些安全性要求比较高的场景下,我们也会选择把逻辑放在 C/C++ 实现,因为 Native 层代码安全性更高,反编译 so 文件的难度比反编译 Class 文件高。
如一些硬件调用的场景下,因为 C/C++ 特性的问题可以直接访问内存/硬件的原因我们也需要使用 C/C++ 实现。
一个标准的 JNI 开发流程:
- 创建对于的 Native 方法
kotlin
external fun stringFromJNI(): String
- 创建对应的CPP文件实现对应的函数 (这里我只讲到App开发常用到的静态注册方式)
c
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_testnative_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
//输出debug级别的日志信息
__android_log_print(ANDROID_LOG_DEBUG, LOG_TGA, "hello form native log");
std::string combinedStr = std::string("测试Print库的信息 - ") + hello;
std::string logStr = log(combinedStr);
return env->NewStringUTF(logStr.c_str());
}
- 在 Cmake/Ndk 中配置源码与链接以及对应的编译名称
go
LOCAL_MODULE := native-lib
- 在Java/Kotin中调用配置编译的动态库名称
kotlin
companion object {
init {
System.loadLibrary("native-lib")
}
}
这都很容易理解,我们难以理解的主要是两方面,一个是关键词与参数,一个是数据转换!
3.1 关键词与参数
arduino
#include <jni.h> // JNI 核心头文件,提供 JNI 数据类型(如 jstring、jobject)和函数声明
#include <string> // C++ 字符串库,支持 std::string 操作
#include <android/log.h> // Android 日志库,用于输出 Logcat 日志
#include "print.h" // 自定义头文件,声明外部函数(如代码中的 log())
const char * LOG_TGA = "LOG_TGA"; // 日志标签常量,用于 Android Logcat 输出
extern "C" // 关键:禁止 C++ 的名称修饰(Name Mangling),确保函数名在动态库中保持原样
JNIEXPORT // 宏定义:标记函数为"可导出",使 Java 能访问此函数
jstring // JNI 类型:对应 Java 的 String 返回值
JNICALL // 宏定义:指定函数调用约定(平台相关,通常无需深究)
Java_com_example_testnative_MainActivity_stringFromJNI( // 静态注册的函数名
JNIEnv *env, // JNI 环境指针,提供所有 JNI 功能的入口(最重要参数)
jobject thiz // 调用此 Native 方法的 Java 对象实例(此处是 MainActivity)
)
JNIEnv 的本质是一个与线程相关的结构体,里面存放了大量的 JNI 函数指针,我们通常通过这个 JNIEnv* 指针去对 Java 端的代码进行操作,比如调用 Java 方法、操作 Java 对象。
jobject thiz,指向该 native 方法的 this 实例,在这里就是MainActivity,比如我们在 MainActivity 调用的下面的 native 函数中打印一下 thiz 的 className:
ini
// 1. 获取 thiz 的 class,也就是 java 中的 Class 信息
jclass thisclazz = env->GetObjectClass(thiz);
// 2. 根据 Class 获取 getClass 方法的 methodID,第三个参数是签名(params)return
jmethodID mid_getClass = env->GetMethodID(thisclazz, "getClass", "()Ljava/lang/Class;");
// 3. 执行 getClass 方法,获得 Class 对象
jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass);
// 4. 获取 Class 实例
jclass clazz = env->GetObjectClass(clazz_instance);
// 5. 根据 class 的 methodID
jmethodID mid_getName = env->GetMethodID(clazz, "getName", "()Ljava/lang/String;");
// 6. 调用 getName 方法
jstring name = static_cast<jstring>(env->CallObjectMethod(clazz_instance, mid_getName));
LOGE("class name:%s", env->GetStringUTFChars(name, 0));
使用 JNIEnv* 指针内置的函数去配合 jobject thiz 的 thiz 对象就可以实现很多骚操作了。
此时打印的 LOG 就是 MainActivity 的类名
*JavaVM ,** 代表 整个 JVM 进程。每个进程只有一个 JavaVM,可以在进程内任何地方(包括不同线程)安全缓存和使用
比如,在 JNI_OnLoad 函数中由 JVM 传入(推荐)
ini
JavaVM *g_jvm = nullptr; // 全局缓存
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
g_jvm = vm; // 保存全局引用
return JNI_VERSION_1_6;
}
通过已有 JNIEnv* 获取:
scss
JavaVM *jvm;
env->GetJavaVM(&jvm); // 从 JNIEnv 获取 JavaVM
典型用法 (在 Native 线程中获取 JNIEnv)
scss
void* nativeThreadFunc(void* arg) {
JNIEnv *env;
// 附加当前线程到 JVM,获取该线程的 JNIEnv
jint result = g_jvm->AttachCurrentThread(&env, nullptr);
if (result == JNI_OK) {
// 现在可以在该线程安全使用 env 了!
env->CallStaticVoidMethod(...);
// ... 其他 JNI 操作 ...
g_jvm->DetachCurrentThread(); // 完成后分离线程
}
return nullptr;
}
3.2 JNI数据类型与转换
由于 Java 和 C/C++ 在数据表示、内存管理和方法调用机制上的差异,JNI 必须进行数据转换以确保两种语言之间的兼容性和正确的交互。这种数据转换是 JNI 使用时的一个关键部分,我们需要了解并正确使用 JNI 提供的接口,以有效地在 Java 和 C/C++ 之间传递数据。
常用的 Java 类型与 JNI 类型的映射关系:

由于 Java 与 C/C++ 默认使用不同的字符编码,因此在操作字符数据时,需要特别注意在 UTF-16 和 UTF-8 两种编码之间转换。
当 Java String 对象转换为 C/C++ 字符串,调用 GetStringUTFChars 函数将一个 jstring 指针转换为一个 UTF-8 的 C/C++ 字符串,并在不再使用时调用 ReleaseStringChars 函数释放内存。
当构建 Java String 对象的时候,调用 NewStringUTF 函数构造一个新的 Java String 字符串对象。
rust
// 示例 1:将 Java String 转换为 C/C++ 字符串
jstring jStr = ...; // Java 层传递过来的 String
const char *str = env->GetStringUTFChars(jStr, JNI_FALSE);
if(!str) {
// OutOfMemoryError
return;
}
// 释放 GetStringUTFChars 生成的 UTF-8 字符串
env->ReleaseStringUTFChars(jStr, str);
// 示例 2:构造 Java String 对象(将 C/C++ 字符串转换为 Java String)
jstring newStr = env->NewStringUTF("在 Native 层构造 Java String");
if (newStr) {
// 通过 JNIEnv 方法将 jstring 调用 Java 方法(jstring 本身就是 Java String 的映射,可以直接传递到 Java 层)
...
}
其他的类型使用 static_cast
ini
Java int 转 C/C++ int
jint javaInt = ...; // 从 Java 获取的整数
int cInt = static_cast<int>(javaInt); // 转换为 C/C++ 整数
C/C++ int 转 Java int
int cInt = ...; // C/C++ 整数
jint javaInt = static_cast<jint>(cInt); // 转换为 Java 整数
jfloat javaFloat = ...; // 从 Java 获取的浮点数
float cFloat = static_cast<float>(javaFloat); // 转换为 C/C++ 浮点数
float cFloat = ...; // C/C++ 浮点数
jfloat javaFloat = static_cast<jfloat>(cFloat); // 转换为 Java 浮点数
jboolean javaBool = ...; // 从 Java 获取布尔值
bool cBool = (javaBool == JNI_TRUE); // 转换为 C/C++ 布尔值
bool cBool = ...; // C/C++ 布尔值
jboolean javaBool = (cBool ? JNI_TRUE : JNI_FALSE); // 转换为 Java 布尔值
如果是数组类型的转换,除了基本数据类型和对象,数组类型也需要进行转换。这包括基本数组(如 int[]、float[])和对象数组(如 String[]、MyObject[])。
ini
jobjectArray javaStringArray = ...; // Java 字符串数组
jsize length = env->GetArrayLength(javaStringArray);
const char** cStringArray = new const char*[length];
for (jsize i = 0; i < length; i++) {
jstring jStr = (jstring)env->GetObjectArrayElement(javaStringArray, i);
const char *str = env->GetStringUTFChars(jStr, JNI_FALSE);
cStringArray[i] = strdup(str); // 复制字符串
env->ReleaseStringUTFChars(jStr, str);
}
C/C++ 字符串数组转 Java String[]
ini
jobjectArray javaStringArray = env->NewObjectArray(length, env->FindClass("java/lang/String"), nullptr);
for (jsize i = 0; i < length; i++) {
jstring jStr = env->NewStringUTF(cStringArray[i]); // 创建 Java 字符串
env->SetObjectArrayElement(javaStringArray, i, jStr); // 设置 Java 数组元素
}
基础数据类型数组:
ini
jintArray javaIntArray = ...; // Java 整数数组
jsize length = env->GetArrayLength(javaIntArray);
jint* cIntArray = new jint[length]; // 创建 C/C++ 整数数组
env->GetIntArrayRegion(javaIntArray, 0, length, cIntArray);
jintArray javaIntArray = env->NewIntArray(length); // 创建 Java 整数数组
env->SetIntArrayRegion(javaIntArray, 0, length, cIntArray); // 从 C/C++ 数组填充 Java 数组
Java 对象转 C/C++ 对象:
ini
jobject javaObject = ...; // 从 Java 获取的对象
jclass javaClass = env->GetObjectClass(javaObject);
jmethodID methodId = env->GetMethodID(javaClass, "getField", "()I");
jint fieldValue = env->CallIntMethod(javaObject, methodId); // 调用 Java 方法获取字段值
C/C++ 对象转 Java 对象
ini
jclass javaClass = env->FindClass("com/example/MyClass");
jmethodID constructorId = env->GetMethodID(javaClass, "<init>", "(I)V");
jobject javaObject = env->NewObject(javaClass, constructorId, cIntValue); // 创建 Java 对象
3.3 JNI的变量引用
JNI 的引用在我们熟悉 Java 语法之后其实很好理解。
JNI 分为三种引用:
- 局部引用(Local Reference),类似 java 中的局部变量
- 全局引用(Global Reference),类似 java 中的全局变量
- 弱全局引用(Weak Global Reference),类似 java 中的弱引用
局部引用:JNI 函数返回的所有 Java 对象都是局部引用,比如上面调用的 NewObject/FindClass/NewStringUTF 等等都是局部引用。
但是在我们GetXXX 之后就必须调用 ReleaseXXX ,记得养成手动释放的好习惯。
全局引用也很好理解,在局部局部引用的基础上我们可以创建一个全局引用
scss
static jstring globalStr;
if(globalStr == NULL){
jstring str = env->NewStringUTF("C++");
// 从局部变量 str 创建一个全局变量
globalStr = static_cast<jstring>(env->NewGlobalRef(str));
//局部可以释放,因为有了一个全局引用使用str,局部str也不会使用了
env->DeleteLocalRef(str);
//当我们不需要的时候也可以释放全局引用
env->DeleteGlobalRef(globalStr);
}
弱全局引用与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象,所以使用之前要判断是否有效。
ini
static jclass globalClazz = NULL;
//对于弱引用 如果引用的对象被回收返回 true,否则为false
//对于局部和全局引用则判断是否引用java的null对象
jboolean isEqual = env->IsSameObject(globalClazz, NULL);
if (globalClazz == NULL || isEqual) {
jclass clazz = env->GetObjectClass(instance);
globalClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz));
//释放使用 DeleteLocalRef 函数
env->DeleteLocalRef(clazz);
}
3.3 JNI的多线程
局部变量只能在当前线程使用,而全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。
scss
// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
// 释放局部引用
env->DeleteLocalRef(localRefClz);
// 局部引用升级为全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
// 释放全局引用
env->DeleteGlobalRef(globalRefClz);
而 JNIEnv 对象只在所在的线程有效,在不同线程中调用 JNI 函数时,必须使用该线程专门的 JNIEnv 指针,不能跨线程传递和使用。通过 AttachCurrentThread 函数将当前线程依附到 JavaVM 上,获得属于当前线程的 JNIEnv 指针。如果当前线程已经依附到 JavaVM,也可以直接使用 GetEnv 函数。
scss
JNIEnv * env_child;
vm->AttachCurrentThread(&env_child, nullptr);
vm->DetachCurrentThread();
创建线程的方式,可以使用 Java 的 API 也可以使用 C/C++ 的 API
ini
// Java 层 - MainActivity.java
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
// Native 方法声明
public native void createJavaThread();
// 将在新线程中执行的方法
public void runInNewThread() {
Log.i("JAVA_THREAD", "Thread name: " + Thread.currentThread().getName());
// 执行耗时操作...
}
}
// Native 层 - native-lib.cpp
#include <jni.h>
#include <android/log.h>
JavaVM* g_jvm = nullptr; // 全局 JavaVM 引用
extern "C" JNIEXPORT void JNICALL
Java_com_example_app_MainActivity_createJavaThread(JNIEnv* env, jobject thiz) {
// 1. 获取 MainActivity 类引用
jclass activityClass = env->GetObjectClass(thiz);
// 2. 获取 runInNewThread 方法 ID
jmethodID methodId = env->GetMethodID(activityClass, "runInNewThread", "()V");
// 3. 创建新线程对象
jclass threadClass = env->FindClass("java/lang/Thread");
jmethodID constructor = env->GetMethodID(threadClass, "<init>", "(Ljava/lang/Runnable;)V");
// 4. 创建 Runnable 对象(使用 lambda 简化)
jclass runnableClass = env->FindClass("java/lang/Runnable");
jmethodID runMethod = env->GetMethodID(runnableClass, "run", "()V");
// 创建匿名 Runnable 实例
jobject runnable = env->NewObject(runnableClass,
env->GetMethodID(runnableClass, "<init>", "()V"));
// 5. 创建线程实例
jobject thread = env->NewObject(threadClass, constructor, runnable);
// 6. 设置线程名(可选)
jmethodID setNameMethod = env->GetMethodID(threadClass, "setName", "(Ljava/lang/String;)V");
jstring threadName = env->NewStringUTF("JavaWorkerThread");
env->CallVoidMethod(thread, setNameMethod, threadName);
env->DeleteLocalRef(threadName);
// 7. 启动线程
jmethodID startMethod = env->GetMethodID(threadClass, "start", "()V");
env->CallVoidMethod(thread, startMethod);
// 8. 清理局部引用
env->DeleteLocalRef(runnable);
env->DeleteLocalRef(thread);
}
另一种方式
arduino
#include <jni.h>
#include <thread>
#include <android/log.h>
JavaVM* g_jvm = nullptr; // 在 JNI_OnLoad 中初始化
void threadFunction() {
// 1. 附加当前线程到 JVM
JNIEnv* env;
int status = g_jvm->AttachCurrentThread(&env, nullptr);
if (status != JNI_OK) return;
// 2. 设置线程名 (Android 特有)
pthread_setname_np(pthread_self(), "NativeWorker");
// 3. 执行任务并打印日志
__android_log_print(ANDROID_LOG_INFO, "NativeThread", "Hello from C++ Thread!");
// 4. 分离线程
g_jvm->DetachCurrentThread();
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_myapp_MainActivity_createStdThread(JNIEnv *env, jobject thiz) {
// 启动线程并立即分离(不阻塞主线程)
std::thread t(threadFunction);
t.detach(); // 让线程独立运行
}
// JNI 初始化
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_jvm = vm;
return JNI_VERSION_1_6;
}
3.4 JNI 的异常处理
JNI 中的异常机制与 Java 和 C/C++ 的处理机制都不同,Java 和 C/C++ 的程序使用关键字 throw 抛出异常,虚拟机会中断当前执行流程。而J NI 函数通过 ThrowNew 抛出异常,程序不会中断当前执行流程,而是返回 Java 层后,虚拟机才会抛出这个异常。
JNI 提供了以下与异常处理相关的 JNI 函数:
- ThrowNew: 向 Java 层抛出异常;
- ExceptionDescribe: 打印异常描述信息;
- ExceptionOccurred: 检查当前环境是否发生异常,如果存在异常则返回该异常对象;
- ExceptionCheck: 检查当前环境是否发生异常,如果存在异常则返回 JNI_TRUE,否则返回 JNI_FALSE;
- ExceptionClear: 清除当前环境的异常。
示例:
scss
extern "C"
JNIEXPORT void JNICALL
Java_com_example_myapp_MainActivity_nativeMethod(JNIEnv *env, jobject thiz) {
// 1. 假设发生错误
bool errorOccurred = true;
if (errorOccurred) {
// 2. 找到要抛出的异常类
jclass exceptionClass = env->FindClass("java/lang/IllegalArgumentException");
if (exceptionClass == nullptr) {
__android_log_print(ANDROID_LOG_ERROR, "JNI_EXCEPTION", "Unable to find exception class");
return;
}
// 3. 抛出异常
env->ThrowNew(exceptionClass, "An error occurred in native code");
// 4. 检测是否发生异常
if (env->ExceptionOccurred()) {
// 异常发生,处理或打印日志
__android_log_print(ANDROID_LOG_ERROR, "JNI_EXCEPTION", "An exception occurred");
env->ExceptionDescribe(); // 打印异常信息
env->ExceptionClear(); // 清除异常
}
// 5. 清理局部引用
env->DeleteLocalRef(exceptionClass);
return;
}
// 其他逻辑
}
我们可以使用 ThrowNew 抛给 Java 处理,也可以通过 ExceptionOccurred 或者 ExceptionCheck 来检测异常自己处理。
四、JNI的实战
讲了这么多东西有点多有点乱,我们用一个示例来整体演示一下。
改项目是一个简单的计时器,使用 C++ 和 JNI 进行实现倒计时,在一个单独的线程中进行倒计时避免堵塞主线程。
Java 调用 JNI 的开始、结束、释放等方法,在 JNI 中调用 Java 的方法回调出去剩余的秒。
我们先用一个 Java 的类来交互 JNI
csharp
public class CountdownTimer {
// 回调接口
public interface CountdownCallback {
void onTick(long secondsLeft);
void onFinish();
}
private CountdownCallback callback;
private volatile boolean isRunning = false;
static {
System.loadLibrary("native-lib");
}
// 设置回调
public void setCallback(CountdownCallback callback) {
this.callback = callback;
}
// JNI方法声明
public native void startCountdown(int seconds);
public native void stopCountdown();
public native void release();
// 供JNI调用的回调方法
private void onNativeTick(long secondsLeft) {
if (callback != null) {
callback.onTick(secondsLeft);
}
}
private void onNativeFinish() {
if (callback != null) {
callback.onFinish();
}
isRunning = false;
}
}
native-lib.cpp 的 JNI 类我们实现倒计时的逻辑并与 Java 进行交互
arduino
#include <jni.h>
#include <atomic>
#include <thread>
#include <chrono>
#include <android/log.h>
#define LOG_TAG "CountdownTimer"
// 全局变量
static std::atomic<bool> running(false);
static JavaVM *jvm = nullptr;
static jobject callbackObj = nullptr;
static jclass timerClass = nullptr;
static jmethodID onTickMethod = nullptr;
static jmethodID onFinishMethod = nullptr;
// 倒计时线程函数
void countdownThread(int seconds)
{
JNIEnv *env;
jvm->AttachCurrentThread(&env, nullptr);
for (int i = seconds; i >= 0; --i)
{
if (!running)
break;
// 调用Java层回调
if (callbackObj && onTickMethod)
{
env->CallVoidMethod(callbackObj, onTickMethod, static_cast<jlong>(i));
}
// 每秒检查一次运行状态
for (int j = 0; j < 10; ++j)
{
if (!running)
break;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// 倒计时结束处理
if (running)
{
if (callbackObj && onFinishMethod)
{
env->CallVoidMethod(callbackObj, onFinishMethod);
}
running = false;
}
jvm->DetachCurrentThread();
}
/// 开始倒计时
extern "C" JNIEXPORT void JNICALL
Java_com_example_testnative_CountdownTimer_startCountdown(JNIEnv *env, jobject obj, jint seconds)
{
if (running)
{
running = false;
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
// 首次调用时初始化全局引用
if (timerClass == nullptr)
{
jclass localClass = env->GetObjectClass(obj);
timerClass = static_cast<jclass>(env->NewGlobalRef(localClass));
env->DeleteLocalRef(localClass);
onTickMethod = env->GetMethodID(timerClass, "onNativeTick", "(J)V");
onFinishMethod = env->GetMethodID(timerClass, "onNativeFinish", "()V");
}
// 保存回调对象
if (callbackObj == nullptr)
{
callbackObj = env->NewGlobalRef(obj);
}
// 获取JVM引用
if (jvm == nullptr)
{
env->GetJavaVM(&jvm);
}
// 启动倒计时
running = true;
std::thread thread(countdownThread, static_cast<int>(seconds));
thread.detach();
}
/// 停止倒计时
extern "C" JNIEXPORT void JNICALL
Java_com_example_testnative_CountdownTimer_stopCountdown(JNIEnv *env, jobject obj)
{
running = false;
}
// 清理资源
extern "C" JNIEXPORT void JNICALL
Java_com_example_testnative_CountdownTimer_release(JNIEnv *env, jobject obj)
{
running = false;
if (callbackObj != nullptr)
{
env->DeleteGlobalRef(callbackObj);
callbackObj = nullptr;
}
if (timerClass != nullptr)
{
env->DeleteGlobalRef(timerClass);
timerClass = nullptr;
}
}
使用的时候 MainActivity:
kotlin
package com.example.testnative
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.testnative.databinding.ActivityMainBinding
@SuppressLint("SetTextI18n")
class MainActivity : AppCompatActivity() {
private lateinit var countdownTimer: CountdownTimer
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
countdownTimer = CountdownTimer()
countdownTimer.setCallback(object : CountdownTimer.CountdownCallback {
override fun onTick(secondsLeft: Long) {
runOnUiThread {
binding.tvTime.text = "剩余时间: $secondsLeft" + "秒"
}
}
override fun onFinish() {
runOnUiThread {
binding.tvTime.text = "倒计时结束!"
}
}
})
binding.btnStart.setOnClickListener {
countdownTimer.startCountdown(60)
}
binding.btnEnd.setOnClickListener {
countdownTimer.stopCountdown()
}
}
override fun onDestroy() {
countdownTimer.stopCountdown()
countdownTimer.release()
super.onDestroy()
}
}
效果:

虽然项目很简单,但是我们把内容都演示到了,Java 调用 JNI (静态注册的方式),JNI 调用 Java 的方式 ,JNIEnv 与 jobject对象 的应用,C/C++线程的使用等等。
总结
在本篇文章中,我们深入探讨了 Android 平台下的 NDK(Native Development Kit)和 JNI(Java Native Interface),并通过一个简单的倒计时器项目展示了如何将 C/C++ 代码与 Java/Kotlin 代码结合使用。以下是我们讨论的主要内容及其重要性:
- NDK 与 JNI 的基础知识:
NDK 使开发者能够在 Android 应用中使用 C/C++ 代码,以优化性能并复用现有的 C/C++ 库。 JNI 是连接 Java 和 C/C++ 的桥梁,允许两者之间进行相互调用和数据交互。
- NDK 的优势:
通过复用高效的 C/C++ 代码,突破 Java 的性能瓶颈,特别是在图形处理、游戏开发和复杂算法实现中。 提高应用的安全性,将核心算法以二进制形式保护,增加反编译的难度。
- 使用 ndk-build 的实战案例:
我们展示了如何使用 ndk-build 方式配置和编译原生代码,包括 Android.mk 文件的编写和 Android Studio 的配置。 通过使用 C++ 代码与 Java 进行互操作,展示了 JNI 的基本使用方法,包括方法调用、数据类型转换和异常处理。
- 倒计时器示例的实现:
通过实现一个简单的倒计时器功能,展示了如何在 JNI 中使用线程来执行耗时操作,并通过回调机制将结果返回到 Java 层。 本示例涵盖了 JNI 的关键概念,如 JNIEnv 和 jobject 的使用,线程的创建与管理,以及如何安全地在不同线程中访问 JNI。
通过本项目,我们掌握了在 Android 开发中结合 NDK 和 JNI 的基本技巧,为未来的更复杂项目打下了基础。了解 C/C++ 的多线程处理和 JNI 的数据流转后,开发者可以在性能要求高的场景中有效地利用原生代码,提高应用的响应速度和用户体验。
了解了这些之后,我们就可以展开分支了,例如结合 OpenGL ES 实现高性能图形渲染 。或者集成 TensorFlow Lite 部署机器学习 模型。又或者使用 FFmpeg 开发音视频处理功能等。
后期我也会计划出一些实战项目的用法,例如网络请求框架,OpenCV,FFmpeg,人脸识别项目,Camera实时滤镜渲染等。
那么今天就这样了,如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
如果感觉本文对你有一点的启发和帮助,还望你能点赞
支持一下!
Ok,完结撒花。
