26. Android NDK交叉编译:使用CMake与Android NDK、CMAKE_TOOLCHAIN_FILE的NDK内置文件
说到Android NDK交叉编译,我最早接触的时候也是一头雾水。那时候还在做一个音视频处理的项目,需要在Android上跑C++代码。我记得当时翻了很多文档,试了好几种方案,最后发现——其实CMake已经帮我们铺好路了。
说白了,Android NDK本身就是一个完整的交叉编译工具链。它包含了针对ARM、x86、x86_64等架构的编译器、链接器、库文件。而CMake要做的,就是通过一个工具链文件,告诉构建系统:「嘿,别用你本机的gcc,用NDK里的这个。」
NDK内置的工具链文件
Google的工程师们早就想到了这一点。他们在NDK里直接提供了一个现成的CMake工具链文件。你不需要自己手写,直接用就行。
这个文件的位置在:
<NDK_PATH>/build/cmake/android.toolchain.cmake
我个人习惯把NDK路径设成环境变量,这样不管换哪个项目,都不用改路径。比如:
export ANDROID_NDK_HOME=/home/user/Android/Sdk/ndk/25.2.9519653
然后在CMake命令行里这样调用:
cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-21 \
-DANDROID_STL=c++_shared \
..
嗯,这里要注意几个关键参数。我一个个说。
核心参数详解
| 参数 | 说明 | 常用值 |
|---|---|---|
| ANDROID_ABI | 目标CPU架构 | arm64-v8a, armeabi-v7a, x86_64, x86 |
| ANDROID_PLATFORM | 最低支持的Android API级别 | android-21, android-24, android-29 |
| ANDROID_STL | C++标准库实现 | c++_shared, c++_static, none |
| ANDROID_LD | 链接器类型(NDK r23+) | lld, default |
ANDROID_ABI 这个参数,说白了就是告诉编译器你要生成哪种CPU的机器码。arm64-v8a是目前主流,覆盖了绝大多数新手机。如果你要兼容老设备,可以再加一个armeabi-v7a。
我在项目中遇到过一个问题:只编译了arm64-v8a,结果测试机是台老平板,直接闪退。后来我学乖了,用Gradle的ABI Filters或者CMake的多ABI构建,一次性打出多个版本。
ANDROID_PLATFORM 决定了你链接的是哪个版本的Android系统库。比如android-21对应Android 5.0。你想想看,如果你用了某个API,但最低平台设得太低,编译时就会报错。反过来,设得太高又会丢失老用户。
ANDROID_STL 这个参数,我刚开始做Android NDK开发时踩过坑。如果你用了std::string、std::vector这些C++标准库的东西,就必须指定STL。c++_shared是动态链接,多个so文件可以共享一份STL,能减小体积。但要注意,如果你的app里只有一个native库,用c++_static反而更省事,不用额外带一个libc++_shared.so。
一个完整的CMakeLists.txt示例
我们来看一个实际项目中的例子。假设你要编译一个图像处理的库:
cmake_minimum_required(VERSION 3.18)
project(ImageProcessor VERSION 1.0.0 LANGUAGES CXX)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加你的库
add_library(image_processor SHARED
src/processor.cpp
src/filter.cpp
src/utils.cpp
)
# 指定头文件路径
target_include_directories(image_processor
PUBLIC
include
PRIVATE
src
)
# 链接系统库
target_link_libraries(image_processor
log
z
android
)
# 如果你用了OpenCV或其他第三方库
# find_package(OpenCV REQUIRED)
# target_link_libraries(image_processor OpenCV::core OpenCV::imgproc)
然后编译命令:
mkdir build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-21 \
-DANDROID_STL=c++_shared \
..
make -j$(nproc)
编译完成后,你会得到一个libimage_processor.so。把它放到Android项目的jniLibs/arm64-v8a/目录下,就可以在Java/Kotlin里通过JNI调用了。
NDK内置工具链文件到底做了什么?
我曾经好奇过,这个android.toolchain.cmake到底干了什么?后来我打开看了一下,发现它主要做了这几件事:
- 设置CMAKE_SYSTEM_NAME为Android
- 根据ANDROID_ABI自动选择对应的编译器(比如aarch64-linux-android21-clang)
- 设置系统根目录(sysroot),指向NDK里的平台库
- 配置链接器、归档器、汇编器等工具路径
- 处理STL的链接方式
- 设置一些编译和链接的flag
说白了,它就是把一堆繁琐的手动配置,封装成了一个文件。你只需要传几个参数,剩下的交给它。
多ABI构建策略
在实际项目中,你通常需要支持多个CPU架构。我常用的做法有两种:
方法一:逐个编译
for abi in arm64-v8a armeabi-v7a x86_64 x86; do
mkdir build_$abi
cd build_$abi
cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=$abi \
-DANDROID_PLATFORM=android-21 \
-DANDROID_STL=c++_shared \
..
make -j$(nproc)
cd ..
done
方法二:使用Gradle + CMake集成
如果你用Android Studio,可以在build.gradle里直接配置:
android {
defaultConfig {
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
abiFilters "arm64-v8a", "armeabi-v7a", "x86_64"
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
我个人更推荐第二种方式。因为Gradle会自动处理ABI的打包,生成APK或AAB时只包含需要的so文件,不会把多余的架构带进去。
避坑指南
- NDK版本不匹配: 有一次我用了NDK r23,但项目里依赖的第三方库是用r21编译的。结果链接时一堆符号找不到。后来我统一了NDK版本,问题解决。
- STL冲突: 如果你的app里加载了多个native库,并且它们用了不同的STL链接方式(一个c++_shared,一个c++_static),运行时可能会崩溃。建议所有库统一使用c++_shared。
- API级别太低: 我试过把ANDROID_PLATFORM设成android-16,结果编译时发现很多C++17的特性不支持。后来我升到android-21,一切正常。
知识体系图
下面这张图展示了Android NDK交叉编译的核心流程和参数关系:
从这张图可以看得很清楚:CMakeLists.txt是你的项目代码,android.toolchain.cmake是NDK提供的桥梁,NDK工具链是底层的编译器集合。三者通过核心参数连接起来,最终输出目标架构的so文件。
总结
Android NDK交叉编译,说白了就是三件事:
- 找到NDK里的工具链文件
- 设置好ABI、平台、STL三个参数
- 正常写你的CMakeLists.txt
剩下的,CMake和NDK会帮你搞定。我刚开始做的时候总觉得这事很神秘,后来发现其实就是一层窗户纸。你只要记住:工具链文件是桥梁,参数是配置,项目代码不用改。掌握了这个思路,以后不管换什么平台,你都能快速上手。
公众号:蓝海资料掘金营,微信deep3321