从入门到实战:CMake 与 Android JNI/NDK 开发全解析

张开发
2026/4/18 18:57:16 15 分钟阅读

分享文章

从入门到实战:CMake 与 Android JNI/NDK 开发全解析
目录前言一、CMake 深度详解现代构建系统的核心1.1 CMake 本质与核心定位1.2 CMake 核心工作流1.3 现代 CMake 核心哲学目标与作用域核心术语1.4 CMake 核心命令全解1.4.1 基础必选命令1.4.2 目标属性配置命令1.4.3 工程管理与依赖命令1.4.4 Android 开发常用内置变量二、Android JNI/NDK 核心原理2.1 基础概念JNI 与 NDK 的区别与联系为什么要在 Android 中使用 JNI/NDK2.2 JNI 核心机制Java 与 C/C 的交互桥梁2.2.1 JNI 函数注册机制静态注册新手入门首选动态注册进阶必备2.2.2 JNI 核心数据类型映射基本类型映射引用类型映射2.2.3 JNI 核心指针JNIEnv三、Android 中 CMake 与 JNI 的深度结合实战3.1 开发环境搭建3.2 标准项目结构3.3 核心配置 1模块级 build.gradle3.4 核心配置 2CMakeLists.txt3.5 编译运行与调试四、最佳实践与高频避坑指南4.1 CMake 开发最佳实践4.2 JNI 开发高频避坑指南4.3 Android NDK 开发优化建议五、总结前言在 Android 开发领域Java/Kotlin 是官方推荐的主流开发语言但涉及高性能计算、音视频编解码、AI 模型推理、硬件驱动访问、跨平台 C/C 库复用等场景时JNI/NDK 开发是无法绕开的核心技能。而 CMake 作为 Google 官方主推的 NDK 构建工具早已替代传统的 ndk-build成为 Android 原生开发的事实标准。本文将从基础原理到实战落地系统讲解 CMake 的核心用法、JNI 的底层机制以及二者在 Android 项目中的深度结合同时补充最佳实践与高频避坑指南无论是刚接触原生开发的新手还是想要巩固进阶的开发者都能从中获得完整的知识体系。一、CMake 深度详解现代构建系统的核心1.1 CMake 本质与核心定位CMakeCross Platform Make是一款开源、跨平台的构建系统生成工具它本身不直接编译代码而是通过统一的配置文件CMakeLists.txt生成对应平台的原生构建文件如 Linux 下的 Makefile、Windows 下的 Visual Studio 工程、macOS 下的 Xcode 工程、Android 下的 Ninja 构建脚本最终调用原生编译器完成编译链接。它的核心优势在于一套配置全平台运行彻底解决跨平台构建的适配问题这也是它成为 Android NDK 首选构建工具的核心原因目标导向的现代设计3.0 版本的「现代 CMake」摒弃了全局变量污染的旧写法以「目标」为核心管理构建属性模块化能力极强生态完善几乎所有主流 C/C 开源库都提供 CMake 配置支持第三方库集成成本极低IDE 友好完美适配 Android Studio、CLion、VS 等主流开发工具支持断点调试、语法高亮、自动补全。1.2 CMake 核心工作流CMake 构建分为标准四步走Android 开发中也是基于这个流程做了封装理解这个流程能帮你快速定位构建问题配置阶段Configure解析顶层CMakeLists.txt检查编译环境、工具链、依赖库是否完备生成构建配置缓存生成阶段Generate根据配置生成对应平台的原生构建文件Android 中默认生成 Ninja 构建脚本构建阶段Build调用原生构建工具Ninja/make/msbuild执行编译、链接操作生成可执行文件或动态 / 静态库安装阶段Install将编译产物、头文件、配置文件安装到指定目录Android 中会自动将生成的 .so 库打包到 APK 中。Android Studio 中点击「Build」按钮时会自动执行上述完整流程我们也可以通过命令行手动执行核心命令如下bash运行# 1. 创建构建目录推荐源外构建隔离源码与构建产物 mkdir build cd build # 2. 配置生成指定NDK工具链Android场景生成构建文件 cmake .. -DCMAKE_TOOLCHAIN_FILE$NDK_PATH/build/cmake/android.toolchain.cmake # 3. 构建多线程编译 cmake --build . -j8 # 4. 安装可选 cmake --install .1.3 现代 CMake 核心哲学目标与作用域现代 CMake 的核心是 **「基于目标Target-based」的设计 **这是它与旧版 CMake 最核心的区别也是新手最容易踩坑的地方。核心术语目标Target通过add_executable()生成的可执行文件、add_library()生成的静态 / 动态库都是 CMake 的目标。所有的构建属性头文件路径、链接库、编译选项都绑定到目标上而非全局设置。属性Properties目标的构建配置比如头文件搜索路径、依赖的链接库、C 标准、编译宏等。作用域Scope控制属性的生效范围分为三类是现代 CMake 最核心的设计表格作用域生效范围PRIVATE仅当前目标自身使用不会传递给依赖该目标的其他目标PUBLIC当前目标自身使用同时传递给所有依赖该目标的其他目标INTERFACE仅传递给依赖该目标的其他目标当前目标自身不使用举个最常见的例子我们编写了一个工具库utils它的头文件放在include目录内部实现的头文件放在src/internal目录。对外暴露的include目录不仅utils自身要用所有依赖utils的目标也要能找到所以用PUBLIC内部的src/internal目录只有utils自己编译时要用不需要对外暴露所以用PRIVATE。对应的 CMake 代码cmakeadd_library(utils STATIC src/utils.cpp src/internal/helper.cpp) # 对外头文件PUBLIC传递给依赖者 target_include_directories(utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) # 内部头文件PRIVATE仅自身使用 target_include_directories(utils PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/internal)这种写法的优势是完全避免全局配置污染依赖传递自动处理模块化能力拉满大型项目中优势极其明显。1.4 CMake 核心命令全解这里整理了 Android JNI 开发中 90% 场景都会用到的核心命令按使用频率排序附带详细用法和场景说明。1.4.1 基础必选命令cmake# 1. 指定CMake最低版本必须放在CMakeLists.txt的第一行 # Android NDK 推荐最低3.18与Android Studio内置版本保持一致 cmake_minimum_required(VERSION 3.22.1) # 2. 定义项目名称、版本、支持的语言 # CXX代表CC代表C语言Android JNI开发通常都要指定 project(MyNativeProject VERSION 1.0.0 LANGUAGES C CXX) # 3. 生成可执行文件Android中极少用主要用于桌面端测试 # 格式add_executable(目标名 源文件1 源文件2 ...) add_executable(my_test test/main.cpp) # 4. 生成库文件Android JNI核心命令 # 格式add_library(目标名 库类型 源文件1 源文件2 ...) # 库类型 # - STATIC静态库(.a)编译时嵌入到目标中不会单独打包 # - SHARED动态库(.so)Android JNI必须用这个类型最终打包到APK # - OBJECT对象库仅编译不链接用于大型项目拆分编译单元 add_library(native-lib SHARED src/native-lib.cpp) # 5. 为目标链接库Android JNI核心命令 # 格式target_link_libraries(目标名 作用域 要链接的库1 库2 ...) # 可以链接自定义目标、系统库、第三方预编译库 target_link_libraries(native-lib PRIVATE log android)1.4.2 目标属性配置命令cmake# 1. 为目标添加头文件搜索路径 # 替代旧版全局的include_directories推荐优先使用 target_include_directories(native-lib PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include $INSTALL_INTERFACE:include PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/internal ) # 2. 为目标指定C标准 # 替代旧版全局的CMAKE_CXX_STANDARD变量更灵活 # 常用值cxx_std_11、cxx_std_14、cxx_std_17、cxx_std_20 target_compile_features(native-lib PUBLIC cxx_std_17) # 3. 为目标添加编译宏定义 # 等价于代码中的 #define DEBUG 1 target_compile_definitions(native-lib PRIVATE DEBUG1 USE_LOG1) # 4. 为目标添加编译选项 # 示例开启所有警告关闭特定警告设置优化等级 target_compile_options(native-lib PRIVATE -Wall -Wextra # 开启所有常规警告 -Wno-unused-parameter # 关闭未使用参数的警告 -O2 # Release模式优化等级Debug模式用-O0 )1.4.3 工程管理与依赖命令cmake# 1. 添加子目录子目录中必须有独立的CMakeLists.txt # 用于大型项目的模块化拆分比如把第三方库、工具库拆分成子模块 add_subdirectory(src/utils) add_subdirectory(third_party/opencv) # 2. 查找系统库或第三方库Android JNI常用 # 格式find_package(库名 版本 REQUIRED) # REQUIRED代表必须找到否则配置失败终止构建 find_package(OpenCV 4.5.5 REQUIRED) # 3. 查找Android系统预编译库 # 示例查找Android系统的日志库liblog.so保存到变量log-lib中 find_library(log-lib log)1.4.4 Android 开发常用内置变量CMake 提供了大量内置变量Android 开发中最常用的如下能帮你解决 90% 的路径问题表格变量名含义CMAKE_SOURCE_DIR顶层 CMakeLists.txt 所在的目录也就是整个工程的根目录CMAKE_CURRENT_SOURCE_DIR当前正在解析的 CMakeLists.txt 所在的目录极其常用PROJECT_SOURCE_DIR最近一次 project () 命令指定的项目根目录CMAKE_BINARY_DIR构建根目录也就是执行 cmake 命令的目录通常是 build 目录ANDROID_ABI当前构建的 CPU 架构如 arm64-v8a、armeabi-v7a用于适配不同设备ANDROID_PLATFORM当前构建的 Android minSdk 版本ANDROID_NDK当前使用的 NDK 根目录路径二、Android JNI/NDK 核心原理很多新手会混淆 JNI 和 NDK这里先明确二者的定义和关系再深入讲解核心机制。2.1 基础概念JNI 与 NDK 的区别与联系JNIJava Native InterfaceJava 本地接口是 Java 语言提供的一套跨语言交互标准定义了 Java 代码与 C/C 等原生代码互相调用的规范是 Java 虚拟机的一部分和 Android 没有直接绑定纯 Java 后端项目也可以使用 JNI。NDKNative Development KitGoogle 为 Android 平台提供的原生开发工具包内置了交叉编译器、标准库、系统 API、构建工具链让开发者可以在 Android 平台上编译、调试 C/C 代码同时提供了完整的 Android 平台适配能力。简单来说JNI 是交互的标准规范NDK 是 Android 平台上实现 JNI 开发的工具集而 CMake 是 NDK 中官方推荐的构建工具。为什么要在 Android 中使用 JNI/NDK它的核心适用场景如下高性能需求音视频编解码、图像算法、AI 模型推理、大数据计算等 CPU 密集型场景C/C 的性能远超 Java/Kotlin跨平台库复用大量成熟的 C/C 开源库如 FFmpeg、OpenCV、TensorFlow Lite、SQLite可以直接复用无需重复造轮子系统底层访问访问 Android 底层硬件驱动、Linux 系统 API、内核空间接口Java 层无法直接触及代码保护Java 代码极易被反编译而 C/C 编译后的 so 库逆向难度极大核心逻辑可以放在原生层保护。2.2 JNI 核心机制Java 与 C/C 的交互桥梁2.2.1 JNI 函数注册机制Java 层声明的native方法需要和 C/C 层的实现函数绑定这个绑定过程就是「注册」分为静态注册和动态注册两种方式。静态注册新手入门首选静态注册是最常用、最简单的注册方式核心是严格遵循 JNI 函数命名规范JVM 会在加载 so 库时自动根据函数名匹配 Java 层的 native 方法。函数命名规范plaintextJava_包名_类名_方法名包名中的点.全部替换为下划线_如果方法名中包含下划线需要替换为_1如果是重载方法需要在末尾添加双下划线__ 参数签名。完整示例Java 层代码MainActivity.javajava运行package com.example.myndkdemo; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends AppCompatActivity { // 1. 声明native方法由C层实现 public native String stringFromJNI(); public native int calculateSum(int a, int b); // 2. 加载编译生成的so库必须在调用native方法前执行 static { System.loadLibrary(native-lib); } Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 3. 调用native方法和调用普通Java方法无区别 TextView tv findViewById(R.id.sample_text); tv.setText(stringFromJNI()); int sum calculateSum(10, 20); } }C 层实现代码native-lib.cppcpp运行#include jni.h #include string // 对应Java的stringFromJNI()方法 // 必须添加extern C防止C编译器对函数名进行名称重整导致JVM找不到函数 extern C JNIEXPORT jstring JNICALL Java_com_example_myndkdemo_MainActivity_stringFromJNI( JNIEnv* env, // JNI核心指针提供了所有JNI操作函数 jobject thiz // 调用该方法的Java对象这里就是MainActivity的实例 ) { std::string hello Hello from C by JNI; // 通过JNIEnv创建Java字符串返回给Java层 return env-NewStringUTF(hello.c_str()); } // 对应Java的calculateSum(int a, int b)方法 extern C JNIEXPORT jint JNICALL Java_com_example_myndkdemo_MainActivity_calculateSum( JNIEnv* env, jobject thiz, jint a, // 对应Java的int参数按顺序排列 jint b ) { return a b; }静态注册的优缺点优点写法简单自动绑定IDE 支持完善Android Studio 可以自动生成函数模板新手不易出错缺点函数名过长编译时才会检查匹配关系运行时找不到方法会直接崩溃修改包名 / 类名 / 方法名时需要同步修改 C 代码。动态注册进阶必备动态注册是手动在 JNI_OnLoad 函数中将 Java 方法和 C 函数绑定无需遵循固定的函数命名规范是大型项目的首选方式。核心原理JVM 加载 so 库时会首先调用JNI_OnLoad函数我们可以在这个函数中手动注册方法映射关系。完整示例cpp运行#include jni.h #include string #include android/log.h // 定义日志宏方便调试 #define LOG_TAG NativeDemo #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) // 1. 定义C实现函数无需遵循静态注册的命名规范 jstring stringFromJNI(JNIEnv* env, jobject thiz) { std::string hello Hello from C by Dynamic Register; return env-NewStringUTF(hello.c_str()); } jint calculateSum(JNIEnv* env, jobject thiz, jint a, jint b) { LOGD(calculateSum: a%d, b%d, a, b); return a b; } // 2. 定义方法映射表 // 格式{Java方法名, 方法签名, C函数指针} static const JNINativeMethod gMethods[] { {stringFromJNI, ()Ljava/lang/String;, (void*)stringFromJNI}, {calculateSum, (II)I, (void*)calculateSum} }; // 3. 重写JNI_OnLoad函数JVM加载so库时会自动调用 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env nullptr; // 从JavaVM获取JNIEnv if (vm-GetEnv((void**)env, JNI_VERSION_1_6) ! JNI_OK) { return JNI_ERR; } // 找到要注册的Java类 const char* className com/example/myndkdemo/MainActivity; jclass clazz env-FindClass(className); if (clazz nullptr) { LOGD(FindClass failed: %s, className); return JNI_ERR; } // 注册方法映射表 int methodNum sizeof(gMethods) / sizeof(gMethods[0]); int ret env-RegisterNatives(clazz, gMethods, methodNum); if (ret ! JNI_OK) { LOGD(RegisterNatives failed: %d, ret); return JNI_ERR; } LOGD(Dynamic register success); // 返回JNI版本必须是JNI_VERSION_1_6及以上 return JNI_VERSION_1_6; }动态注册的优缺点优点函数名灵活修改 Java 类名 / 方法名无需修改 C 函数名注册时就会检查匹配关系崩溃风险低大型项目中便于管理缺点写法相对复杂需要手动维护方法映射表新手容易出错。2.2.2 JNI 核心数据类型映射Java 是运行在 JVM 上的类型安全语言而 C/C 是原生语言二者的类型系统完全不同JNI 定义了一套完整的类型映射规则保证数据在二者之间正确传递。基本类型映射基本类型可以直接传递无需转换一一对应表格Java 基本类型JNI 类型字节数说明booleanjboolean1布尔值JNI_TRUE1JNI_FALSE0bytejbyte18 位有符号整数charjchar216 位无符号字符shortjshort216 位有符号整数intjint432 位有符号整数longjlong864 位有符号整数floatjfloat432 位浮点数doublejdouble864 位浮点数voidvoid0无返回值引用类型映射Java 的引用类型对象、数组、字符串在 JNI 中都有对应的引用类型不能直接使用必须通过JNIEnv提供的函数进行转换和操作表格Java 引用类型JNI 类型说明java.lang.StringjstringJava 字符串类型java.lang.Objectjobject任意 Java 对象的基类型java.lang.ClassjclassJava 类类型java.lang.ThrowablejthrowableJava 异常类型基本类型数组对应j类型Array如int[]→jintArraybyte[]→jbyteArray对象数组jobjectArray任意 Java 对象的数组2.2.3 JNI 核心指针JNIEnvJNIEnv*是 JNI 开发中最核心的指针它指向 JVM 内部的函数表提供了所有 JNI 操作的函数比如创建字符串、操作数组、访问 Java 对象的字段和方法、处理异常等。关于JNIEnv的关键注意事项线程绑定JNIEnv是线程私有的不能跨线程传递和使用子线程要使用 JNIEnv必须通过JavaVM附加到 JVM 上获取生命周期JNIEnv的生命周期和当前线程绑定线程退出后对应的JNIEnv自动失效JavaVM是 JVM 的全局代表一个进程只有一个JavaVM对象可以跨线程传递和使用是子线程获取JNIEnv的唯一方式。子线程获取JNIEnv的示例代码cpp运行JavaVM* g_jvm; // 全局保存JavaVM在JNI_OnLoad中赋值 // JNI_OnLoad中保存JavaVM JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { g_jvm vm; // ... 其他逻辑 return JNI_VERSION_1_6; } // 子线程函数 void* threadFunc(void* arg) { JNIEnv* env nullptr; // 将当前线程附加到JVM获取JNIEnv if (g_jvm-AttachCurrentThread(env, nullptr) ! JNI_OK) { return nullptr; } // ... 在这里使用env执行JNI操作 // 线程退出前必须分离否则会内存泄漏 g_jvm-DetachCurrentThread(); return nullptr; }三、Android 中 CMake 与 JNI 的深度结合实战3.1 开发环境搭建在 Android Studio 中进行 JNICMake 开发需要提前安装以下工具打开 Android Studio → 顶部菜单Tools→SDK Manager切换到SDK Tools标签页勾选以下工具NDK (Side by side)推荐选择 LTS 版本如 r25c、r26b避免最新版的兼容性问题CMake选择 3.18 版本与 NDK 版本匹配Android SDK Build-Tools最新稳定版点击Apply等待安装完成Android Studio 会自动配置环境变量。3.2 标准项目结构Android JNICMake 项目有固定的标准结构遵循这个结构可以避免大量路径问题和构建错误plaintextMyNDKDemo/ ├── app/ # 主模块 │ ├── src/ │ │ └── main/ │ │ ├── java/ # Java/Kotlin代码 │ │ │ └── com/example/myndkdemo/MainActivity.java │ │ ├── cpp/ # C/C源码目录核心 │ │ │ ├── CMakeLists.txt # JNI模块的CMake配置文件 │ │ │ ├── native-lib.cpp # JNI实现代码 │ │ │ ├── include/ # 对外头文件 │ │ │ └── utils/ # 工具类子模块 │ │ │ └── CMakeLists.txt │ │ ├── res/ # 资源文件 │ │ └── AndroidManifest.xml # 清单文件 │ └── build.gradle # 模块级Gradle配置核心 └── build.gradle # 项目级Gradle配置3.3 核心配置 1模块级 build.gradle要让 Android Studio 识别并执行 CMake 构建必须在模块级build.gradle中配置 CMake 关联这是连接 Gradle 与 CMake 的桥梁。完整配置如下逐行附带说明groovyplugins { id com.android.application } android { namespace com.example.myndkdemo compileSdk 34 defaultConfig { applicationId com.example.myndkdemo minSdk 24 targetSdk 34 versionCode 1 versionName 1.0 testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner // 【核心1】NDK构建的默认配置作用于所有构建变体 externalNativeBuild { cmake { // 指定C标准必须和CMakeLists.txt中保持一致 cppFlags -stdc17 // 指定要构建的CPU架构目前主流Android设备仅需保留这两个 // armeabi-v7a32位ARM设备兼容老旧设备 // arm64-v8a64位ARM设备目前主流机型 abiFilters arm64-v8a, armeabi-v7a // 传递给CMake的参数比如定义宏、指定路径 arguments -DDEBUG1 } } // 【可选】指定打包到APK中的CPU架构和abiFilters保持一致即可 ndk { abiFilters arm64-v8a, armeabi-v7a } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } // 【核心2】关联CMakeLists.txt文件告诉Android Studio CMake配置的位置 externalNativeBuild { cmake { // CMakeLists.txt的相对路径必须正确 path file(src/main/cpp/CMakeLists.txt) // 指定CMake版本必须和SDK Manager中安装的版本一致 version 3.22.1 } } } dependencies { // ... 常规依赖 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 }关键注意事项很多新手会混淆两个externalNativeBuild配置块这里明确区分defaultConfig内部的externalNativeBuild配置构建参数比如 C 标准、ABI 架构、CMake 启动参数android根节点的externalNativeBuild配置CMake 配置文件的路径和版本是关联 CMake 的入口。3.4 核心配置 2CMakeLists.txt这是 JNI 模块构建的核心文件Android Studio 会根据这个文件编译 C/C 代码生成 so 库并自动打包到 APK 中。这里提供一个生产级完整配置包含基础构建、系统库链接、子模块管理、第三方库集成逐行附带详细说明cmake# 1. 指定CMake最低版本必须和build.gradle中指定的版本一致 cmake_minimum_required(VERSION 3.22.1) # 2. 定义项目名称和支持的语言 project(myndkdemo) # 3. 全局C标准配置也可以用target_compile_features针对目标配置 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 强制要求该标准不支持则配置失败 set(CMAKE_CXX_EXTENSIONS OFF) # 关闭GNU扩展保证跨平台兼容性 # 4. 生成JNI核心动态库最终会生成libnative-lib.so # 格式add_library(库名 SHARED 源文件列表) # 注意库名必须和Java中System.loadLibrary(库名)保持一致 add_library( native-lib SHARED native-lib.cpp utils/math_utils.cpp ) # 5. 为目标添加头文件搜索路径 target_include_directories(native-lib PUBLIC # 构建时的头文件路径当前CMakeLists.txt所在目录的include文件夹 ${CMAKE_CURRENT_SOURCE_DIR}/include PRIVATE # 内部工具类的头文件路径 ${CMAKE_CURRENT_SOURCE_DIR}/utils ) # 6. 查找Android系统库 # 6.1 查找系统日志库liblog.so用于C层打印日志到Logcat find_library(log-lib log) # 6.2 查找Android原生库libandroid.so用于访问Android系统API find_library(android-lib android) # 6.3 查找图形库libGLESv3.so用于OpenGL渲染 find_library(GLESv3-lib GLESv3) # 7. 添加子模块子模块有自己的CMakeLists.txt add_subdirectory(utils) # 8. 【进阶】集成第三方预编译库以OpenCV为例 # 8.1 设置OpenCV SDK的路径这里放在cpp目录下的third_party/opencv set(OpenCV_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/opencv/sdk/native/jni) # 8.2 查找OpenCV库 find_package(OpenCV REQUIRED) # 9. 为目标链接所有需要的库 # 注意链接顺序要符合依赖关系被依赖的库放在后面 target_link_libraries( native-lib # 链接OpenCV库 ${OpenCV_LIBS} # 链接Android系统库 ${log-lib} ${android-lib} ${GLESv3-lib} )3.5 编译运行与调试完成上述配置后点击 Android Studio 顶部的「运行」按钮会自动执行以下流程Gradle 调用 CMake解析CMakeLists.txt生成 Ninja 构建脚本CMake 调用 NDK 中的交叉编译器编译 C/C 代码生成对应 ABI 架构的libnative-lib.so库Gradle 将生成的 so 库打包到 APK 中安装 APK 到设备启动 App执行System.loadLibrary(native-lib)加载 so 库调用 native 方法。调试技巧Android Studio 完美支持 JNI 断点调试只需在Run/Debug Configurations中将调试类型设置为Dual (Java Native)即可同时在 Java 代码和 C 代码中打断点调试查看变量、调用栈和普通 Java 调试完全一致。四、最佳实践与高频避坑指南4.1 CMake 开发最佳实践坚持使用现代 CMake 写法所有属性绑定到目标上用target_*系列命令绝对不要使用全局的include_directories、link_directories、add_definitions等命令避免全局污染严格区分作用域对外暴露的头文件用PUBLIC内部使用的用PRIVATE仅对外提供接口的用INTERFACE不要滥用PUBLIC始终使用源外构建不要在源码目录执行 CMake 命令创建独立的 build 目录隔离源码和构建产物便于清理和版本控制版本匹配CMake 版本、NDK 版本、Android minSdk 版本必须匹配避免兼容性问题NDK r25 推荐 minSdk ≥ 24模块化拆分大型项目中将不同功能拆分成独立的子模块每个子模块有自己的CMakeLists.txt通过add_subdirectory引入便于维护和复用。4.2 JNI 开发高频避坑指南这是新手开发中 90% 崩溃问题的来源务必牢记必须添加extern CC 代码中的 JNI 函数必须包裹在extern C中否则 C 编译器会对函数名进行名称重整导致 JVM 找不到方法运行时直接崩溃严格遵循函数命名规范静态注册时包名、类名、方法名必须完全匹配包名中的点替换为下划线否则会出现UnsatisfiedLinkError崩溃JNI 引用必须正确释放局部引用NewStringUTF、FindClass、NewObject等函数创建的局部引用使用完后必须用DeleteLocalRef释放尤其是在循环中否则会导致局部引用表溢出触发 OOM全局引用需要跨线程、跨函数使用的 Java 对象必须用NewGlobalRef创建全局引用不用时用DeleteGlobalRef释放否则会内存泄漏JNIEnv 不能跨线程使用JNIEnv是线程私有的跨线程传递使用会直接崩溃子线程必须通过JavaVM的AttachCurrentThread方法获取当前线程的JNIEnv线程退出前必须调用DetachCurrentThread异常必须处理JNI 函数执行后可能会抛出 Java 异常必须用ExceptionCheck检查是否有异常用ExceptionDescribe打印异常用ExceptionClear清除异常否则后续的 JNI 操作会直接崩溃数组操作必须正确释放用GetByteArrayElements获取数组指针后必须用ReleaseByteArrayElements释放否则会内存泄漏同时注意参数mode的使用避免数据丢失不要在 JNI 中阻塞主线程JNI 函数是在调用它的 Java 线程中执行的在主线程中执行耗时的 JNI 操作会导致 ANR耗时操作必须放在子线程中执行。4.3 Android NDK 开发优化建议ABI 架构优化目前 Google Play 要求必须支持 64 位架构国内应用市场也逐步要求推荐仅保留arm64-v8a主流 64 位和armeabi-v7a兼容 32 位两个架构减少 APK 体积so 库体积优化Release 模式下开启代码混淆和裁剪在build.gradle中设置minifyEnabled true同时配置proguard-rules.pro保留 native 方法在 CMake 中添加编译选项-fvisibilityhidden隐藏不必要的符号减小 so 体积使用strip命令去除 so 库中的调试符号Release 版本必须 strip代码保护核心逻辑放在 C 层同时开启 OLLVM 混淆、字符串加密、反调试等保护措施提高逆向难度性能优化减少 Java 和 C 之间的频繁数据传递尤其是大数组和字符串尽量减少跨边界调用次数避免在循环中执行 JNI 调用尽量把循环逻辑放在 C 层音视频、图像等高性能场景使用 NEON 指令集优化充分发挥 ARM 架构的性能。五、总结CMake 与 JNI/NDK 是 Android 高级开发的核心技能也是进入音视频、AI、物联网、高性能计算等领域的必备基础。本文从 CMake 的现代设计理念到 JNI 的底层交互机制再到 Android 项目中的完整实战落地系统讲解了整个知识体系同时补充了高频踩坑点和最佳实践。对于新手来说建议先从静态注册 基础 CMake 配置入手跑通第一个 JNI 项目再逐步学习动态注册、第三方库集成、性能优化等进阶内容。原生开发的核心是「严谨」很多崩溃问题都来自于细节的疏忽严格遵循规范和最佳实践能帮你避开 90% 以上的问题。后续我会继续分享 JNI 进阶内容比如 Java 与 C 双向对象互调、FFmpeg 音视频解码、OpenCV 图像处理、TensorFlow Lite 模型推理等实战内容欢迎持续关注。

更多文章