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

目录

前言

[一、CMake 深度详解:现代构建系统的核心](#一、CMake 深度详解:现代构建系统的核心)

[1.1 CMake 本质与核心定位](#1.1 CMake 本质与核心定位)

[1.2 CMake 核心工作流](#1.2 CMake 核心工作流)

[1.3 现代 CMake 核心哲学:目标与作用域](#1.3 现代 CMake 核心哲学:目标与作用域)

核心术语

[1.4 CMake 核心命令全解](#1.4 CMake 核心命令全解)

[1.4.1 基础必选命令](#1.4.1 基础必选命令)

[1.4.2 目标属性配置命令](#1.4.2 目标属性配置命令)

[1.4.3 工程管理与依赖命令](#1.4.3 工程管理与依赖命令)

[1.4.4 Android 开发常用内置变量](#1.4.4 Android 开发常用内置变量)

[二、Android JNI/NDK 核心原理](#二、Android JNI/NDK 核心原理)

[2.1 基础概念:JNI 与 NDK 的区别与联系](#2.1 基础概念:JNI 与 NDK 的区别与联系)

[为什么要在 Android 中使用 JNI/NDK?](#为什么要在 Android 中使用 JNI/NDK?)

[2.2 JNI 核心机制:Java 与 C/C++ 的交互桥梁](#2.2 JNI 核心机制:Java 与 C/C++ 的交互桥梁)

[2.2.1 JNI 函数注册机制](#2.2.1 JNI 函数注册机制)

静态注册(新手入门首选)

动态注册(进阶必备)

[2.2.2 JNI 核心数据类型映射](#2.2.2 JNI 核心数据类型映射)

基本类型映射

引用类型映射

[2.2.3 JNI 核心指针:JNIEnv](#2.2.3 JNI 核心指针:JNIEnv)

[三、Android 中 CMake 与 JNI 的深度结合实战](#三、Android 中 CMake 与 JNI 的深度结合实战)

[3.1 开发环境搭建](#3.1 开发环境搭建)

[3.2 标准项目结构](#3.2 标准项目结构)

[3.3 核心配置 1:模块级 build.gradle](#3.3 核心配置 1:模块级 build.gradle)

[3.4 核心配置 2:CMakeLists.txt](#3.4 核心配置 2:CMakeLists.txt)

[3.5 编译运行与调试](#3.5 编译运行与调试)

四、最佳实践与高频避坑指南

[4.1 CMake 开发最佳实践](#4.1 CMake 开发最佳实践)

[4.2 JNI 开发高频避坑指南](#4.2 JNI 开发高频避坑指南)

[4.3 Android NDK 开发优化建议](#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 本质与核心定位

CMake(Cross 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 开发中也是基于这个流程做了封装,理解这个流程能帮你快速定位构建问题:

  1. 配置阶段(Configure) :解析顶层 CMakeLists.txt,检查编译环境、工具链、依赖库是否完备,生成构建配置缓存;
  2. 生成阶段(Generate):根据配置,生成对应平台的原生构建文件(Android 中默认生成 Ninja 构建脚本);
  3. 构建阶段(Build):调用原生构建工具(Ninja/make/msbuild),执行编译、链接操作,生成可执行文件或动态 / 静态库;
  4. 安装阶段(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 代码:

cmake

复制代码
add_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代表C++,C代表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 DEBUG=1 USE_LOG=1)

# 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 的区别与联系

  • JNI(Java Native Interface) :Java 本地接口,是 Java 语言提供的一套跨语言交互标准,定义了 Java 代码与 C/C++ 等原生代码互相调用的规范,是 Java 虚拟机的一部分,和 Android 没有直接绑定,纯 Java 后端项目也可以使用 JNI。
  • NDK(Native Development Kit) :Google 为 Android 平台提供的原生开发工具包,内置了交叉编译器、标准库、系统 API、构建工具链,让开发者可以在 Android 平台上编译、调试 C/C++ 代码,同时提供了完整的 Android 平台适配能力。

简单来说:JNI 是交互的标准规范,NDK 是 Android 平台上实现 JNI 开发的工具集,而 CMake 是 NDK 中官方推荐的构建工具。

为什么要在 Android 中使用 JNI/NDK?

它的核心适用场景如下:

  1. 高性能需求:音视频编解码、图像算法、AI 模型推理、大数据计算等 CPU 密集型场景,C/C++ 的性能远超 Java/Kotlin;
  2. 跨平台库复用:大量成熟的 C/C++ 开源库(如 FFmpeg、OpenCV、TensorFlow Lite、SQLite)可以直接复用,无需重复造轮子;
  3. 系统底层访问:访问 Android 底层硬件驱动、Linux 系统 API、内核空间接口,Java 层无法直接触及;
  4. 代码保护:Java 代码极易被反编译,而 C/C++ 编译后的 so 库逆向难度极大,核心逻辑可以放在原生层保护。

2.2 JNI 核心机制:Java 与 C/C++ 的交互桥梁

2.2.1 JNI 函数注册机制

Java 层声明的 native 方法,需要和 C/C++ 层的实现函数绑定,这个绑定过程就是「注册」,分为静态注册动态注册两种方式。

静态注册(新手入门首选)

静态注册是最常用、最简单的注册方式,核心是严格遵循 JNI 函数命名规范,JVM 会在加载 so 库时,自动根据函数名匹配 Java 层的 native 方法。

函数命名规范

plaintext

复制代码
Java_<包名>_<类名>_<方法名>
  • 包名中的点 . 全部替换为下划线 _
  • 如果方法名中包含下划线,需要替换为 _1
  • 如果是重载方法,需要在末尾添加双下划线 __ + 参数签名。

完整示例 :Java 层代码(MainActivity.java):

java

运行

复制代码
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.cpp):

cpp

运行

复制代码
#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 类型 字节数 说明
boolean jboolean 1 布尔值,JNI_TRUE=1,JNI_FALSE=0
byte jbyte 1 8 位有符号整数
char jchar 2 16 位无符号字符
short jshort 2 16 位有符号整数
int jint 4 32 位有符号整数
long jlong 8 64 位有符号整数
float jfloat 4 32 位浮点数
double jdouble 8 64 位浮点数
void void 0 无返回值
引用类型映射

Java 的引用类型(对象、数组、字符串)在 JNI 中都有对应的引用类型,不能直接使用,必须通过 JNIEnv 提供的函数进行转换和操作:

表格

Java 引用类型 JNI 类型 说明
java.lang.String jstring Java 字符串类型
java.lang.Object jobject 任意 Java 对象的基类型
java.lang.Class jclass Java 类类型
java.lang.Throwable jthrowable Java 异常类型
基本类型数组 对应 j<类型>Array int[]jintArraybyte[]jbyteArray
对象数组 jobjectArray 任意 Java 对象的数组
2.2.3 JNI 核心指针:JNIEnv

JNIEnv* 是 JNI 开发中最核心的指针,它指向 JVM 内部的函数表,提供了所有 JNI 操作的函数,比如创建字符串、操作数组、访问 Java 对象的字段和方法、处理异常等。

关于 JNIEnv 的关键注意事项:

  1. 线程绑定JNIEnv 是线程私有的,不能跨线程传递和使用,子线程要使用 JNIEnv,必须通过 JavaVM 附加到 JVM 上获取;
  2. 生命周期JNIEnv 的生命周期和当前线程绑定,线程退出后,对应的 JNIEnv 自动失效;
  3. 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 中进行 JNI+CMake 开发,需要提前安装以下工具:

  1. 打开 Android Studio → 顶部菜单 ToolsSDK Manager
  2. 切换到 SDK Tools 标签页,勾选以下工具:
    • NDK (Side by side):推荐选择 LTS 版本(如 r25c、r26b),避免最新版的兼容性问题;
    • CMake:选择 3.18+ 版本,与 NDK 版本匹配;
    • Android SDK Build-Tools:最新稳定版;
  3. 点击 Apply 等待安装完成,Android Studio 会自动配置环境变量。

3.2 标准项目结构

Android JNI+CMake 项目有固定的标准结构,遵循这个结构可以避免大量路径问题和构建错误:

plaintext

复制代码
MyNDKDemo/
├── 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 的桥梁。

完整配置如下,逐行附带说明:

groovy

复制代码
plugins {
    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 "-std=c++17"
                // 指定要构建的CPU架构,目前主流Android设备仅需保留这两个
                // armeabi-v7a:32位ARM设备,兼容老旧设备
                // arm64-v8a:64位ARM设备,目前主流机型
                abiFilters 'arm64-v8a', 'armeabi-v7a'
                // 传递给CMake的参数,比如定义宏、指定路径
                arguments "-DDEBUG=1"
            }
        }

        // 【可选】指定打包到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 核心配置 2:CMakeLists.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 顶部的「运行」按钮,会自动执行以下流程:

  1. Gradle 调用 CMake,解析 CMakeLists.txt,生成 Ninja 构建脚本;
  2. CMake 调用 NDK 中的交叉编译器,编译 C/C++ 代码,生成对应 ABI 架构的 libnative-lib.so 库;
  3. Gradle 将生成的 so 库打包到 APK 中;
  4. 安装 APK 到设备,启动 App,执行 System.loadLibrary("native-lib") 加载 so 库,调用 native 方法。

调试技巧 :Android Studio 完美支持 JNI 断点调试,只需在 Run/Debug Configurations 中,将调试类型设置为 Dual (Java + Native),即可同时在 Java 代码和 C++ 代码中打断点调试,查看变量、调用栈,和普通 Java 调试完全一致。


四、最佳实践与高频避坑指南

4.1 CMake 开发最佳实践

  1. 坚持使用现代 CMake 写法 :所有属性绑定到目标上,用 target_* 系列命令,绝对不要使用全局的 include_directorieslink_directoriesadd_definitions 等命令,避免全局污染;
  2. 严格区分作用域 :对外暴露的头文件用 PUBLIC,内部使用的用 PRIVATE,仅对外提供接口的用 INTERFACE,不要滥用 PUBLIC
  3. 始终使用源外构建:不要在源码目录执行 CMake 命令,创建独立的 build 目录,隔离源码和构建产物,便于清理和版本控制;
  4. 版本匹配:CMake 版本、NDK 版本、Android minSdk 版本必须匹配,避免兼容性问题,NDK r25+ 推荐 minSdk ≥ 24;
  5. 模块化拆分 :大型项目中,将不同功能拆分成独立的子模块,每个子模块有自己的 CMakeLists.txt,通过 add_subdirectory 引入,便于维护和复用。

4.2 JNI 开发高频避坑指南

这是新手开发中 90% 崩溃问题的来源,务必牢记:

  1. 必须添加 extern "C" :C++ 代码中的 JNI 函数,必须包裹在 extern "C" 中,否则 C++ 编译器会对函数名进行名称重整,导致 JVM 找不到方法,运行时直接崩溃;
  2. 严格遵循函数命名规范 :静态注册时,包名、类名、方法名必须完全匹配,包名中的点替换为下划线,否则会出现 UnsatisfiedLinkError 崩溃;
  3. JNI 引用必须正确释放
    • 局部引用:NewStringUTFFindClassNewObject 等函数创建的局部引用,使用完后必须用 DeleteLocalRef 释放,尤其是在循环中,否则会导致局部引用表溢出,触发 OOM;
    • 全局引用:需要跨线程、跨函数使用的 Java 对象,必须用 NewGlobalRef 创建全局引用,不用时用 DeleteGlobalRef 释放,否则会内存泄漏;
  4. JNIEnv 不能跨线程使用JNIEnv 是线程私有的,跨线程传递使用会直接崩溃,子线程必须通过 JavaVMAttachCurrentThread 方法获取当前线程的 JNIEnv,线程退出前必须调用 DetachCurrentThread
  5. 异常必须处理 :JNI 函数执行后,可能会抛出 Java 异常,必须用 ExceptionCheck 检查是否有异常,用 ExceptionDescribe 打印异常,用 ExceptionClear 清除异常,否则后续的 JNI 操作会直接崩溃;
  6. 数组操作必须正确释放 :用 GetByteArrayElements 获取数组指针后,必须用 ReleaseByteArrayElements 释放,否则会内存泄漏,同时注意参数 mode 的使用,避免数据丢失;
  7. 不要在 JNI 中阻塞主线程:JNI 函数是在调用它的 Java 线程中执行的,在主线程中执行耗时的 JNI 操作,会导致 ANR,耗时操作必须放在子线程中执行。

4.3 Android NDK 开发优化建议

  1. ABI 架构优化 :目前 Google Play 要求必须支持 64 位架构,国内应用市场也逐步要求,推荐仅保留 arm64-v8a(主流 64 位)和 armeabi-v7a(兼容 32 位)两个架构,减少 APK 体积;
  2. so 库体积优化
    • Release 模式下开启代码混淆和裁剪,在 build.gradle 中设置 minifyEnabled true,同时配置 proguard-rules.pro 保留 native 方法;
    • 在 CMake 中添加编译选项 -fvisibility=hidden,隐藏不必要的符号,减小 so 体积;
    • 使用 strip 命令去除 so 库中的调试符号,Release 版本必须 strip;
  3. 代码保护:核心逻辑放在 C++ 层,同时开启 OLLVM 混淆、字符串加密、反调试等保护措施,提高逆向难度;
  4. 性能优化
    • 减少 Java 和 C++ 之间的频繁数据传递,尤其是大数组和字符串,尽量减少跨边界调用次数;
    • 避免在循环中执行 JNI 调用,尽量把循环逻辑放在 C++ 层;
    • 音视频、图像等高性能场景,使用 NEON 指令集优化,充分发挥 ARM 架构的性能。

五、总结

CMake 与 JNI/NDK 是 Android 高级开发的核心技能,也是进入音视频、AI、物联网、高性能计算等领域的必备基础。本文从 CMake 的现代设计理念,到 JNI 的底层交互机制,再到 Android 项目中的完整实战落地,系统讲解了整个知识体系,同时补充了高频踩坑点和最佳实践。

对于新手来说,建议先从静态注册 + 基础 CMake 配置入手,跑通第一个 JNI 项目,再逐步学习动态注册、第三方库集成、性能优化等进阶内容。原生开发的核心是「严谨」,很多崩溃问题都来自于细节的疏忽,严格遵循规范和最佳实践,能帮你避开 90% 以上的问题。

后续我会继续分享 JNI 进阶内容,比如 Java 与 C++ 双向对象互调、FFmpeg 音视频解码、OpenCV 图像处理、TensorFlow Lite 模型推理等实战内容,欢迎持续关注。

相关推荐
冬奇Lab2 小时前
相机录像流程:MediaRecorder与Camera2的协作之道
android·音视频开发·源码阅读
麦客奥德彪3 小时前
Jetpack Compose 常用开发总结
android
麦客奥德彪3 小时前
Jetpack Compose Modifier 完全指南
android
风曦Kisaki4 小时前
# Linux 磁盘查看命令详解:df 与 du
linux·运维·网络
路溪非溪5 小时前
Linux中gpio子系统的现代接口
linux·arm开发·驱动开发
筱璦5 小时前
期货软件开发 - C# 调用 HQChart 指标计算 C++ 动态库
c++·microsoft·c#
不想写代码的星星5 小时前
C++ 内存管理:分区、自定义分配器、常见问题与检测工具
c++
Mac的实验室5 小时前
(2026年最新)解决谷歌账号注册设备扫码短信发送失败无法验证难题(100%通过无需扫码验证)
android·google·程序员
-许平安-6 小时前
MCP项目笔记九(插件 bacio-quote)
c++·笔记·ai·plugin·mcp