NDK 构建管线与 CMake 工具链:C++ 怎样变成 Android 能加载的 .so
这一篇先解决最基础的问题:你写了一段 C++,Android Studio 到底怎样把它变成手机能运行的 native 库?
把构建过程想成一条生产线。
C++ 源码
-> 编译器 clang++
-> 目标文件 .o
-> 链接器 lld
-> 共享库 libxxx.so
-> APK/AAB 打包
-> App 启动时 System.loadLibrary 加载
什么是 .so
.so 是 shared object,共享库。它不是源码,也不是普通压缩包,而是一种 ELF 二进制文件。
Android 运行时加载 .so 时,会做这些事。
把文件映射进进程地址空间
检查它属于哪个 ABI
解析它依赖哪些系统库或三方库
解析符号地址
执行必要的初始化逻辑
如果 .so ABI 不匹配、缺依赖、符号找不到,App 可能在启动时直接崩溃。
Android Studio 里谁负责构建 native 代码
常见项目链路是:
Gradle
-> Android Gradle Plugin
-> externalNativeBuild
-> CMake
-> NDK toolchain
-> clang/lld
Gradle 负责总调度。
AGP 负责 Android 构建规则。
CMake 负责 C/C++ 工程描述。
NDK toolchain 提供 Android 版 clang、sysroot、头文件、链接库。
官方 CMake 文档说明,NDK 的 toolchain 文件位于:
<NDK>/build/cmake/android.toolchain.cmake
这个文件会告诉 CMake:现在不是给 macOS 或 Windows 编译,而是给 Android 指定 ABI 和 API level 编译。
最小 CMakeLists.txt
cmake_minimum_required(VERSION 3.22.1)
project(zerobug_player)
add_library(
player_core
SHARED
player_core.cpp
)
find_library(log-lib log)
target_link_libraries(
player_core
${log-lib}
)
逐行解释。
add_library(... SHARED ...) 表示生成 .so。
player_core 会变成 libplayer_core.so。
find_library(log-lib log) 找 Android 系统日志库。
target_link_libraries 把日志库链接进来,C++ 才能调用 __android_log_print。
Gradle 怎样接入 CMake
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags += "-std=c++17"
}
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
}
这段配置的意思是:AGP 构建 App 时,同时调用 CMake 去构建 native 代码。
ANDROID_ABI 是什么
ANDROID_ABI 决定你要给哪类 CPU 和调用约定编译。
常见值:
arm64-v8a:现代 Android 手机主力 64 位 ARM ABI
armeabi-v7a:老 32 位 ARM ABI
x86_64:模拟器常用 64 位 x86 ABI
同一份 C++ 源码可以编译出多份 .so。
lib/arm64-v8a/libplayer_core.so
lib/armeabi-v7a/libplayer_core.so
lib/x86_64/libplayer_core.so
手机安装后只加载自己 ABI 对应的那份。
ANDROID_PLATFORM 与 minSdk
ANDROID_PLATFORM 表示 native 代码要面向哪个 Android API level 构建。官方文档提醒:NDK 库不能运行在低于该值的设备上。
如果你编译时选择 android-26,却让 App 跑在 API 23 设备上,native 代码可能调用到设备没有的系统符号。
保守做法:
ANDROID_PLATFORM 与 minSdk 保持一致或更谨慎
使用新 API 时显式做版本判断或动态探测
Debug、Release、RelWithDebInfo
native 构建类型会影响调试和性能。
Debug:便于调试,优化少,体积大
Release:优化强,符号通常被剥离,线上常用
RelWithDebInfo:有优化,也保留调试信息,适合性能分析和崩溃还原
对 NDK 项目来说,RelWithDebInfo 很有价值。它既接近线上性能,又能给 ndk-stack、simpleperf 提供符号。
第一个 native 函数
#include <android/log.h>
extern "C" void player_core_hello() {
__android_log_print(ANDROID_LOG_INFO, "PlayerCore", "native library loaded");
}
这里的 extern "C" 是为了避免 C++ 名字改编。C++ 会把函数名编码成带参数类型的信息,方便重载;但 JNI 或动态查找场景通常需要稳定符号名。
本章实验
你可以先构建一个只打印日志的 .so。
确认 app/build/intermediates/cxx 下出现 obj/<abi>/libplayer_core.so
确认 APK 中有 lib/<abi>/libplayer_core.so
确认 System.loadLibrary("player_core") 不崩溃
如果加载失败,按这个顺序排查。
库名是否写错:player_core 对应 libplayer_core.so
ABI 是否匹配:设备 ABI 是否有对应 so
依赖库是否缺失:UnsatisfiedLinkError 是否提示找不到其他 so
minSdk/API 是否不匹配
工程风险与观测
构建链路最容易被低估。很多 native 问题不是 C++ 写错,而是构建参数悄悄变了。
建议每次发布都记录这些信息。
NDK version
CMake version
AGP version
ANDROID_ABI
ANDROID_PLATFORM
build type
STL
这些信息像 native 产物的出生证明。线上崩溃需要回滚或符号化时,它能帮你确定这份 .so 是怎样来的。
最常见工程风险:
本机 Debug 能跑,Release 因符号或优化崩溃
arm64-v8a 能构建,x86_64 因平台相关代码失败
minSdk 改了,但 ANDROID_PLATFORM 没跟着审计
三方库换版本后,依赖 .so 没被打包
解决方式不是靠记忆,而是把构建参数写进 CI 产物。
构建完成后输出 native-build-manifest.txt
每个 ABI 记录 .so 路径和 sha256
RelWithDebInfo 符号归档
Release 包和符号文件建立一一对应关系
当线上需要回滚时,你不是“重新构建一个差不多的包”,而是能找到当时发布的确切产物。
本章结论
NDK 构建不是“点一下 Run 就结束”。你需要知道 C++ 如何经过 clang、lld、CMake、AGP 变成 .so,以及 ABI 和 API level 怎样进入最终产物。后面所有 native 问题,几乎都能回到这条生产线定位。