目录
[一、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 开发中也是基于这个流程做了封装,理解这个流程能帮你快速定位构建问题:
- 配置阶段(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 代码:
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?
它的核心适用场景如下:
- 高性能需求:音视频编解码、图像算法、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 方法。
函数命名规范:
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[] → jintArray,byte[] → jbyteArray |
| 对象数组 | jobjectArray |
任意 Java 对象的数组 |
2.2.3 JNI 核心指针:JNIEnv
JNIEnv* 是 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 中进行 JNI+CMake 开发,需要提前安装以下工具:
- 打开 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 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 顶部的「运行」按钮,会自动执行以下流程:
- 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 "C":C++ 代码中的 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 中添加编译选项
-fvisibility=hidden,隐藏不必要的符号,减小 so 体积; - 使用
strip命令去除 so 库中的调试符号,Release 版本必须 strip;
- Release 模式下开启代码混淆和裁剪,在
- 代码保护:核心逻辑放在 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 模型推理等实战内容,欢迎持续关注。