学习笔记,关于NDK/JNI的简介与实战

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官方提供的原生代码开发套件,她可以做哪些事情?

  1. 复用C/C++代码:直接集成现有C/C++库(如OpenCV、FFmpeg)。
  2. 突破性能瓶颈,更能发挥硬件极限性能。
  3. 增强安全性,通过编译后的二进制文件保护核心算法,反编译难度高于Java字节码。

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

一般来说 JNI 是一大块内容,这个单独章节来讲,关于 NDK 的编译主流也是分为 ndk-build/CMake 两种。

  1. ndk-build 是 NDK 提供的一种传统的构建系统,基于 Android.mkApplication.mk 文件。这套构建系统较早期使用,适合小型或中等规模的项目。

  2. 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 开发流程:

  1. 创建对于的 Native 方法
kotlin 复制代码
    external fun stringFromJNI(): String
  1. 创建对应的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());
}
  1. 在 Cmake/Ndk 中配置源码与链接以及对应的编译名称
go 复制代码
LOCAL_MODULE := native-lib
  1. 在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 分为三种引用:

  1. 局部引用(Local Reference),类似 java 中的局部变量
  2. 全局引用(Global Reference),类似 java 中的全局变量
  3. 弱全局引用(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 函数:

  1. ThrowNew: 向 Java 层抛出异常;
  2. ExceptionDescribe: 打印异常描述信息;
  3. ExceptionOccurred: 检查当前环境是否发生异常,如果存在异常则返回该异常对象;
  4. ExceptionCheck: 检查当前环境是否发生异常,如果存在异常则返回 JNI_TRUE,否则返回 JNI_FALSE;
  5. 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 代码结合使用。以下是我们讨论的主要内容及其重要性:

  1. NDK 与 JNI 的基础知识:

NDK 使开发者能够在 Android 应用中使用 C/C++ 代码,以优化性能并复用现有的 C/C++ 库。 JNI 是连接 Java 和 C/C++ 的桥梁,允许两者之间进行相互调用和数据交互。

  1. NDK 的优势:

通过复用高效的 C/C++ 代码,突破 Java 的性能瓶颈,特别是在图形处理、游戏开发和复杂算法实现中。 提高应用的安全性,将核心算法以二进制形式保护,增加反编译的难度。

  1. 使用 ndk-build 的实战案例:

我们展示了如何使用 ndk-build 方式配置和编译原生代码,包括 Android.mk 文件的编写和 Android Studio 的配置。 通过使用 C++ 代码与 Java 进行互操作,展示了 JNI 的基本使用方法,包括方法调用、数据类型转换和异常处理。

  1. 倒计时器示例的实现:

通过实现一个简单的倒计时器功能,展示了如何在 JNI 中使用线程来执行耗时操作,并通过回调机制将结果返回到 Java 层。 本示例涵盖了 JNI 的关键概念,如 JNIEnv 和 jobject 的使用,线程的创建与管理,以及如何安全地在不同线程中访问 JNI。

通过本项目,我们掌握了在 Android 开发中结合 NDK 和 JNI 的基本技巧,为未来的更复杂项目打下了基础。了解 C/C++ 的多线程处理和 JNI 的数据流转后,开发者可以在性能要求高的场景中有效地利用原生代码,提高应用的响应速度和用户体验。

了解了这些之后,我们就可以展开分支了,例如结合 OpenGL ES 实现高性能图形渲染 。或者集成 TensorFlow Lite 部署机器学习 模型。又或者使用 FFmpeg 开发音视频处理功能等。

后期我也会计划出一些实战项目的用法,例如网络请求框架,OpenCV,FFmpeg,人脸识别项目,Camera实时滤镜渲染等。

那么今天就这样了,如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下!

Ok,完结撒花。

相关推荐
追随远方11 分钟前
Android高性能音频与图形开发:OpenSL ES与OpenGL ES最佳实践
android·elasticsearch·音视频
钓鱼的肝23 分钟前
题单:归并排序
c++·算法
kymjs张涛1 小时前
前沿技术周刊 2025-06-09
android·前端·ios
随意0231 小时前
STL 6分配器
开发语言·c++
jndingxin2 小时前
c++ 面试题(1)-----深度优先搜索(DFS)实现
c++·算法·深度优先
用户2018792831672 小时前
View的filterTouchesWhenObscured属性
android
Watink Cpper2 小时前
[灵感源于算法] 算法问题的优雅解法
linux·开发语言·数据结构·c++·算法·leetcode
老一岁2 小时前
C++ 类与对象的基本概念和使用
java·开发语言·c++
随意0232 小时前
STL 3算法
开发语言·c++·算法
偷懒下载原神2 小时前
《C++ 继承》
开发语言·c++