一、CMakeLists.txt 基础:从零搭建你的第一个NDK项目

说实话,我刚开始接触Android NDK开发时,最头疼的就是CMakeLists.txt。那时候网上资料少,踩了不少坑。后来做了几个大型项目,才慢慢摸清楚这里面的门道。今天咱们就从最基础的东西讲起。

1.1 cmake_minimum_required() —— 版本声明

这个命令说白了就是告诉CMake:“嘿,你得用至少我这个版本才能编译”。我个人习惯在文件第一行就写上它。

cmake_minimum_required(VERSION 3.18.1)

为什么要指定版本?我在项目中遇到过这样的事:同事用了个新版本的CMake特性,结果老版本编译环境直接报错。所以,版本声明不是摆设,是团队协作的保险

注意:Android NDK目前推荐的最低CMake版本是3.18.1。如果你用太老的版本,有些NDK特性可能不支持。

1.2 project() —— 给你的项目起个名字

project()命令用来定义项目名称,同时还会设置一些内部变量。比如:

project(MyNativeLib VERSION 1.0.0 LANGUAGES C CXX)

这里我指定了项目名、版本号,还有使用的语言。CXX就是C++。你想想看,如果不告诉CMake你要用C++,它可能连标准库都找不到。

小技巧:项目名会生成两个变量:PROJECT_NAME 和 PROJECT_SOURCE_DIR。我在做多模块项目时经常用它们来定位源码路径。

1.3 add_library() —— 创建你的库

这是最核心的命令。它告诉CMake:“我要编译一个库,源码是这些”。

add_library(
    native-lib          # 库的名字
    SHARED              # 共享库(.so)
    src/main/cpp/native-lib.cpp
    src/main/cpp/utils.cpp
)

库的类型有三种:

类型 说明 生成文件
SHARED 共享库,会被多个进程共用 .so
STATIC 静态库,编译时直接嵌入 .a
MODULE 插件库,运行时按需加载 .so

嗯,这里要注意:Android上绝大多数情况用SHARED。静态库在NDK里用得少,因为Android的加载机制对.so更友好。

1.4 target_link_libraries() —— 链接依赖

你的库不可能单打独斗。比如要用到Android的Log系统,就得链接log库:

target_link_libraries(
    native-lib
    log                 # Android日志库
    android             # Android基础库
    ${CMAKE_CURRENT_SOURCE_DIR}/libs/libfoo.a   # 静态库
)

我曾经犯过一个低级错误:链接顺序写反了。结果链接器报了一堆未定义符号。后来才明白,链接顺序是有讲究的——被依赖的库要放在后面。

核心原则:target_link_libraries() 不仅链接库,还会传递头文件搜索路径和编译选项。这是现代CMake的“目标传递”机制。

1.5 基本语法规则

CMake的语法其实很简单,但有几个坑要注意:

  • 命令不区分大小写:add_library 和 ADD_LIBRARY 一样。但我建议全小写,看着舒服。
  • 参数用空格分隔:别用逗号!我第一次写CMake就用了逗号,结果报错半天。
  • 字符串用双引号:如果路径里有空格,必须加引号。比如 "C:/Program Files/..."
  • 注释用 #:和Shell脚本一样。
  • 变量用 ${}:引用变量时用 ${VAR_NAME}
# 这是一个完整的例子
cmake_minimum_required(VERSION 3.18.1)
project(HelloNDK)

add_library(hello SHARED hello.cpp)

target_link_libraries(hello log android)

1.6 知识体系总览

下面这张图是我自己总结的CMakeLists.txt核心结构。你看一遍就能明白各个命令之间的关系:

CMakeLists.txt 核心结构 CMakeLists.txt cmake_minimum_required() project() 其他全局设置 add_library() — 定义你的库 target_link_libraries() — 链接依赖 编译产物:.so / .a 从上到下,依次执行:版本检查 → 项目定义 → 库创建 → 依赖链接 → 生成产物

1.7 一个完整的实战例子

说了这么多,不如直接看个完整的。这是我早期做的一个日志工具库:

cmake_minimum_required(VERSION 3.18.1)

project(LogUtils VERSION 1.2.0 LANGUAGES CXX)

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

# 添加源码
add_library(
    logutils
    SHARED
    src/log_utils.cpp
    src/log_writer.cpp
    include/log_utils.h
)

# 指定头文件路径
target_include_directories(
    logutils
    PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# 链接系统库
target_link_libraries(
    logutils
    log
    android
)
个人经验:我习惯把头文件路径用 target_include_directories() 单独设置,而不是写在 add_library() 里。这样更清晰,也方便后续扩展。

1.8 避坑指南

最后分享几个我踩过的坑:

  • 路径问题:CMake里的路径都是相对于CMakeLists.txt所在目录的。别用绝对路径,否则换台机器就编译不过。
  • 库名冲突:别用系统库的名字,比如别叫“log”或“android”。我见过有人这么干,结果链接时一片混乱。
  • 忘记PRIVATE/PUBLIC:target_link_libraries() 如果不指定可见性,默认是PRIVATE。但显式写出来更安全。
  • 大小写敏感:虽然命令不区分大小写,但变量名是区分大小写的。比如 ${CMAKE_SOURCE_DIR}${cmake_source_dir} 不一样。

好了,这一章的内容就到这里。记住:CMakeLists.txt 是NDK项目的骨架,骨架搭好了,后面的工作就顺了。


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