49、Android NDK:使用CMake编译Android原生库

说实话,Android开发这么多年,我一直觉得纯Java/Kotlin写应用挺舒服的。但一旦涉及到性能敏感的场景——比如图像处理、音视频编解码、或者游戏引擎——你就不得不请出C++这位老大哥了。

我记得第一次在Android项目里集成C++代码时,用的是老旧的Android.mk + ndk-build那套。说实话,那体验真不怎么样。后来Google官方开始推荐CMake,我试了一下,嗯,这才对味嘛。

今天我们就聊聊,怎么用CMake在Android NDK环境下编译原生库。

49.1 环境准备:你需要的工具

在动手之前,先把家伙事儿备齐。你需要:

  • Android Studio(3.0以上版本,越新越好)
  • NDK(Native Development Kit)
  • CMake(Android Studio自带,也可以单独装)

安装NDK很简单:打开Android Studio → SDK Manager → SDK Tools → 勾选NDK和CMake → Apply。等它下载完就行了。

小提示:我个人习惯在项目的local.properties里指定NDK路径,这样团队里其他人拉下来也能直接用。格式是:ndk.dir=/path/to/ndk

49.2 项目结构:CMakeLists.txt放哪儿?

一个典型的Android NDK项目结构是这样的:

app/
├── src/
│   └── main/
│       ├── cpp/
│       │   ├── CMakeLists.txt
│       │   ├── native-lib.cpp
│       │   └── include/
│       └── java/
└── build.gradle

CMakeLists.txt放在src/main/cpp/目录下,这是Android Studio的默认约定。当然你也可以放别处,但需要在build.gradle里指定路径。

49.3 编写CMakeLists.txt:从零开始

好,我们直接上手写一个。假设我们要编译一个叫native-lib的库,它依赖一个第三方库libpng

cmake_minimum_required(VERSION 3.10)

# 设置项目名
project("my_native_lib")

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加原生库
add_library(
    native-lib
    SHARED
    native-lib.cpp
    utils.cpp
)

# 查找系统库(比如log)
find_library(
    log-lib
    log
)

# 链接库
target_link_libraries(
    native-lib
    ${log-lib}
)

这段代码干了三件事:

  • 声明了一个叫native-lib的共享库
  • 指定了源文件
  • 链接了Android的log库(方便打印日志)

嗯,这里要注意:SHARED表示编译成动态库(.so),如果你想要静态库就用STATIC。我在项目中一般用动态库,因为可以减少APK体积,而且多个库之间可以共享代码。

49.4 在build.gradle中集成CMake

光有CMakeLists.txt还不够,你得告诉Gradle去调用CMake。在app/build.gradleandroid块里加上:

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-O2 -frtti -fexceptions"
                abiFilters "arm64-v8a", "armeabi-v7a", "x86_64"
            }
        }
    }

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.22.1"
        }
    }
}

这里有几个关键点:

  • cppFlags:传给C++编译器的参数。-O2是优化级别,-frtti-fexceptions分别启用RTTI和异常支持。我个人建议除非必要,否则别开异常,因为会增加二进制体积。
  • abiFilters:指定要编译的CPU架构。现在主流是arm64-v8a,但为了兼容老设备,我一般会加上armeabi-v7a。x86_64主要是给模拟器用的。
注意:我曾经踩过一个坑——忘了加abiFilters,结果CMake把所有架构都编译了一遍,APK体积直接翻倍。所以一定要显式指定你需要的架构。

49.5 多库依赖:链接第三方库

实际项目中,你很少只编译一个孤零零的库。比如你要用OpenCV或者FFmpeg,就得处理依赖关系。

假设我们有一个预编译的libpng.so放在app/src/main/jniLibs/目录下,CMakeLists.txt里这样写:

# 导入预编译库
add_library(
    png
    SHARED
    IMPORTED
)

set_target_properties(
    png
    PROPERTIES
    IMPORTED_LOCATION
    ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libpng.so
)

# 链接到我们的库
target_link_libraries(
    native-lib
    png
    ${log-lib}
)

这里用IMPORTED关键字告诉CMake:这个库不是我编译的,是外面拿来的。然后通过IMPORTED_LOCATION指定它的路径。

你想想看,如果每个架构的libpng.so都放在不同目录,用${ANDROID_ABI}这个变量就能自动匹配,是不是很方便?

49.6 知识体系:一张图看懂

说了这么多,我画张图帮你理清整个流程:

Android NDK + CMake 编译流程 C/C++ 源码 .cpp / .c / .h CMakeLists.txt 构建配置 NDK 工具链 clang / ld / ar .so 动态库 arm64-v8a / armeabi-v7a / x86_64 build.gradle externalNativeBuild { cmake { ... } } APK 打包 lib/arm64-v8a/libnative-lib.so Java/Kotlin 代码 System.loadLibrary("native-lib") 整个流程:源码 → CMake配置 → NDK编译 → .so → 打包进APK → Java调用

49.7 避坑指南:我踩过的那些坑

做NDK开发,坑是真的不少。我挑几个常见的说说:

  • STL链接问题:如果你用了std::stringstd::vector,记得在CMakeLists.txt里加上set(CMAKE_SHARED_LINKER_FLAGS "-static-libstdc++"),否则运行时可能崩溃。
  • JNI函数命名:Java层的native方法名必须和C++函数名严格对应。比如com.example.MyClass.nativeMethod对应Java_com_example_MyClass_nativeMethod。少一个下划线都不行。
  • ABI不匹配:我曾经把arm64的.so放到了armeabi-v7a的目录下,结果在旧设备上直接闪退。所以一定要检查jniLibs目录结构是否正确。
核心要点:CMake + NDK的本质,就是用CMake管理C++代码的编译过程,然后通过Gradle集成到Android构建流程中。你只需要写好CMakeLists.txt,剩下的交给工具链。

49.8 调试与验证

编译完成后,怎么确认.so文件真的打进了APK?

用Android Studio的APK Analyzer:Build → Analyze APK → 选择你的apk文件。然后展开lib/目录,就能看到各个架构下的.so文件了。

或者用命令行:

unzip -l app-debug.apk | grep .so

如果看到了libnative-lib.so,恭喜你,成功了。

嗯,其实NDK开发没有想象中那么复杂。你只要把CMakeLists.txt写对,Gradle配置好,剩下的就是写C++代码的事了。我个人觉得,CMake比Android.mk直观太多了,至少你不用记那些奇怪的语法。

好了,这一章就到这里。去试试吧,把第一个.so编译出来,你会很有成就感的。


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