18、NDK与CMake集成:NDK构建流程、CMakeLists.txt编写、ABI过滤与兼容、JNI库构建优化
说实话,NDK 这块内容,很多 Android 开发者一开始都是绕道走的。我当年也一样,觉得 Java/Kotlin 写得好好的,干嘛要去碰 C/C++?直到第一次遇到需要做图像处理、音视频编解码或者复用现有 C 库的时候,才意识到 NDK 是绕不开的坎。
这一章,我们就来把 NDK 和 CMake 的集成这件事彻底讲透。从构建流程到脚本编写,再到 ABI 兼容和性能优化,我会把我在项目中踩过的坑、总结的经验都放进来。
NDK 构建流程:从源码到 .so
先理清楚 NDK 构建到底干了什么。说白了,就是把你的 C/C++ 源码,编译成 Android 能用的动态库(.so 文件)。
整个流程大致是这样的:
- 配置阶段:Gradle 读取
build.gradle中的externalNativeBuild配置,找到 CMakeLists.txt 文件。 - CMake 配置:CMake 根据 CMakeLists.txt 中的指令,生成 Ninja 构建文件(Android 现在默认用 Ninja 而不是 Make)。
- 编译阶段:Ninja 调用编译器(Clang)和链接器,针对你指定的 ABI(如 arm64-v8a)编译源码,生成 .so 文件。
- 打包阶段:Gradle 将生成的 .so 文件打包进 APK 或 AAB 中对应的 ABI 目录下。
核心要点:整个流程是 Gradle → CMake → Ninja → Clang 的链式调用。你只需要写好 CMakeLists.txt 和 build.gradle 配置,剩下的交给工具链。
嗯,这里要注意一个细节:NDK 版本和 AGP(Android Gradle Plugin)版本有兼容关系。我遇到过几次因为 NDK 版本太新,导致某些旧版 CMake 模块找不到的问题。建议使用 AGP 推荐的 NDK 版本,或者统一用 NDK 25+ 搭配 AGP 8.x。
CMakeLists.txt 编写:从入门到熟练
CMakeLists.txt 是 NDK 构建的核心配置文件。我刚开始写的时候,觉得这玩意儿语法挺别扭的,但用多了发现其实套路很固定。
一个最基础的 CMakeLists.txt 长这样:
cmake_minimum_required(VERSION 3.22.1)
project("my_native_lib")
add_library(
my_native_lib
SHARED
native-lib.cpp
utils.cpp
)
find_library(
log-lib
log
)
target_link_libraries(
my_native_lib
${log-lib}
)
我来拆解一下这几行命令:
cmake_minimum_required:指定 CMake 最低版本。建议设成 3.22.1 以上,因为新版 CMake 对 Android 的支持更好。project:项目名称,随便起,但最好和你的模块名一致。add_library:核心命令。第一个参数是库名,第二个参数SHARED表示生成动态库(.so),后面跟源码文件列表。find_library:查找系统预编译库,比如 Android 的 log 库。target_link_libraries:链接依赖库。把 log 库链接到你的 native 库上,这样你才能在 C++ 代码里调用__android_log_print。
个人习惯:我会把源码文件单独列一个变量,比如 set(SOURCES native-lib.cpp utils.cpp),然后在 add_library 里引用 ${SOURCES}。这样后续加文件时只需要改一处,不容易漏。
如果你有多个库,或者需要引入第三方预编译库,可以这样写:
add_library(
my_native_lib
SHARED
native-lib.cpp
)
# 引入预编译的 .a 静态库
add_library(
prebuilt_lib
STATIC
IMPORTED
)
set_target_properties(
prebuilt_lib
PROPERTIES
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libprebuilt.a
)
target_link_libraries(
my_native_lib
prebuilt_lib
${log-lib}
)
这里 ${ANDROID_ABI} 是 CMake 自动提供的变量,表示当前正在构建的 ABI 名称(比如 arm64-v8a)。利用它,你可以为不同架构提供不同的预编译库路径。
ABI 过滤与兼容:别让你的 APK 变得臃肿
ABI(Application Binary Interface)决定了你的 .so 文件能在哪些 CPU 架构上运行。Android 支持的 ABI 主要有:
| ABI 名称 | 对应架构 | 说明 |
|---|---|---|
| armeabi-v7a | 32位 ARM | 老设备、低端机,兼容性最好 |
| arm64-v8a | 64位 ARM | 目前主流,性能最强 |
| x86 | 32位 x86 | 模拟器、少数平板 |
| x86_64 | 64位 x86 | 模拟器、Chromebook |
你想想看,如果四个 ABI 都打进去,APK 体积至少增加几十 MB。所以,ABI 过滤是必须做的。
在 build.gradle 中配置 ABI 过滤:
android {
defaultConfig {
ndk {
// 只保留 arm64-v8a 和 armeabi-v7a
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
}
我曾经踩过的坑:只保留 arm64-v8a,结果在部分老机型上直接崩溃。后来发现那些设备虽然是 64 位 CPU,但系统运行在 32 位模式下,根本加载不了 64 位的 .so。所以,如果你的应用需要兼容 Android 5.0 以下的设备,建议保留 armeabi-v7a。
另外,ABI 兼容性还有一个容易被忽略的点:如果你的 native 库依赖了其他第三方 .so(比如 OpenCV、FFmpeg),这些库也必须提供对应 ABI 的版本。否则编译时链接通过,运行时直接 UnsatisfiedLinkError。
JNI 库构建优化:让性能再飞一会儿
JNI(Java Native Interface)是 Java/Kotlin 和 C/C++ 之间的桥梁。但这座桥是有代价的——每次 JNI 调用都有开销。优化 JNI 库,说白了就是减少开销、提升效率。
我总结了几条实战优化策略:
- 减少 JNI 调用次数:能一次调用传回一个数组,就别分十次调用每次传一个值。我在做图像处理时,会把整帧数据通过
ByteBuffer一次性传过去,而不是逐像素调用。 - 使用
GetDirectBufferAddress:如果 Java 层使用DirectByteBuffer,C++ 层可以直接拿到内存地址,省去数据拷贝。这对音视频、大数据量场景特别有用。 - 缓存 jclass 和 jmethodID:每次 JNI 调用都去查找类和方法,开销很大。我习惯在
JNI_OnLoad中缓存这些 ID,用全局变量保存。 - 避免在关键路径上使用
FindClass:FindClass是一个相对重的操作,尽量在初始化时完成。
来看一个缓存 jclass 的示例:
static jclass g_clazz;
static jmethodID g_callback_method;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
// 缓存 jclass
jclass clazz = env->FindClass("com/example/MyNativeLib");
g_clazz = (jclass)env->NewGlobalRef(clazz);
// 缓存方法 ID
g_callback_method = env->GetMethodID(g_clazz, "onNativeCallback", "(I)V");
return JNI_VERSION_1_6;
}
我建议:在 JNI_OnLoad 中做所有初始化工作,包括加载依赖库、缓存 ID、初始化全局状态。这样后续的 JNI 调用可以做到“零准备”,直接干活。
还有一个容易被忽视的点:编译优化选项。在 CMakeLists.txt 中,可以添加:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -fvisibility=hidden -ffunction-sections -fdata-sections")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections")
-O2 开启优化,-fvisibility=hidden 隐藏非导出符号(减小 .so 体积),--gc-sections 去除未使用的代码段。这些选项组合起来,能让你的 .so 体积缩小 20%-30%,同时运行速度也有提升。
知识体系总览
我把这一章的核心逻辑画了张图,方便你整体把握:
这张图把四个核心模块串起来了。从左到右是构建流程的推进,从上到下是配置与优化的递进。你可以把它当作一个检查清单——每次集成 NDK 时,对照着过一遍,基本不会漏掉关键步骤。
好了,NDK 与 CMake 集成这块内容就讲到这里。从构建流程到脚本编写,从 ABI 过滤到 JNI 优化,每一步都有不少细节。但只要你按照这套方法去实践,遇到问题基本都能找到根因。毕竟,这些坑我都替你踩过了。