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_PLATFORM 设成 android-21 就够了。这个版本覆盖了99%以上的活跃设备。

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

说白了,它就是把一堆繁琐的手动配置,封装成了一个文件。你只需要传几个参数,剩下的交给它。

核心要点: 使用NDK内置工具链文件,你不需要自己写任何交叉编译的配置。只需要设置ANDROID_ABI、ANDROID_PLATFORM、ANDROID_STL这三个参数,CMake就能自动完成交叉编译环境的搭建。

多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交叉编译的核心流程和参数关系:

Android NDK交叉编译核心流程 CMakeLists.txt android.toolchain.cmake NDK工具链 核心参数配置 ANDROID_ABI ANDROID_PLATFORM ANDROID_STL ANDROID_LD CMake 配置 → 生成构建系统 → 编译 → 链接 libxxx.so (目标架构) 支持 arm64-v8a | armeabi-v7a | x86_64 | x86

从这张图可以看得很清楚:CMakeLists.txt是你的项目代码,android.toolchain.cmake是NDK提供的桥梁,NDK工具链是底层的编译器集合。三者通过核心参数连接起来,最终输出目标架构的so文件。

总结

Android NDK交叉编译,说白了就是三件事:

  • 找到NDK里的工具链文件
  • 设置好ABI、平台、STL三个参数
  • 正常写你的CMakeLists.txt

剩下的,CMake和NDK会帮你搞定。我刚开始做的时候总觉得这事很神秘,后来发现其实就是一层窗户纸。你只要记住:工具链文件是桥梁,参数是配置,项目代码不用改。掌握了这个思路,以后不管换什么平台,你都能快速上手。

小提示: 如果你在Android Studio里用CMake,其实IDE会自动帮你传这些参数。但理解背后的原理,能让你在遇到问题时快速定位。比如编译报错说找不到某个头文件,你就能想到是不是ANDROID_PLATFORM设得太低了。

公众号:蓝海资料掘金营,微信deep3321