在 Android NDK 开发中, CMake 是帮助我们来生成 makefle 文件的, 本文的示例是在 windows 中进行的, CMake 安装相关的可以参考本文 Windows10下配置CMake+Make+Cpp环境, 这里不做介绍

初识

使用 CMake 最重要的文件之一是 CMakeLists.txt 文件, 所以我们直接先创建好文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
cmake_minimum_required(VERSION 3.16) 指定 cmake 最低版本
# 给工程取一个名字
PROJECT (HELLO)

# 定义一个变量
SET(SRC_LIST hello.cpp)

# 打印
MESSAGE(STATUS "this is BINARY dir "${HELLO_BINARY_DIR})
MESSAGE(STATUS "this is SOURCE dir "${HELLO_SOURCE_DIR})
MESSAGE(STATUS "this is PROJECT_SOURCE dir "${PROJECT_SOURCE_DIR})

# 生成可执行文件
ADD_EXECUTABLE(hello.out ${SRC_LIST})

其中 hello.cpp 的文件只有一个日志输出

1
2
3
4
5
6
#include <stdio.h>
int main()
{
  printf("hello cmake\n");
  return 0;
}

文件目录结构如下, 然后我们使用 cmd 定位到该目录下, 开始进行编译

1
cmake . -G "MinGW Makefiles" // 注意这里要加 -G "MinGW Makefiles", 不然在 Windows 下会没有 makefile 文件的生成, 其中 . 是代表的当前目录

运行完成只有, 就会得到一个 makefile 文件

然后再执行 make, 其实就是执行 makefile 文件了

1
make

然后就会得到一个 hello.out 的输出文件

最后再运行, 输出如下

到这里, 我们通过 CMake 就构建生成了一个简单 C++ 项目, 步骤并不复杂, 最重要的是需要掌握 CMakeLists.txt 如何编写, 接下来看看它们都代表什么意思

常见 CMakeLists 语法

注:CMakeLists 对大小写没有限制

  • cmake_minimum_required 表示支持的 cmake 最小版本,这里是 3.16

    1
    
    cmake_minimum_required(VERSION 3.16)
  • PROJECT 表示项目名称,这里是 HELLO

    1
    
    PROJECT (HELLO)
  • MESSAGE 是用来打印输出信息的, 这里的 xxx_BINARY_DIR 前面都需要带上项目的名称, 其中 HELLO_SOURCE_DIR 和 PROJECT_SOURCE_DIR 是一样的, 代表的是 CMakeLists 文件所在的目录, BINARY_DIR 表示的是构建目录, 也就是 makefile 文件所在的目录, 一般情况下如果通过 cmake 指定在不同的文件中生成构建文件, 它们两路径就会不一致

    1
    2
    3
    
    MESSAGE(STATUS "this is BINARY dir "${HELLO_BINARY_DIR})
    MESSAGE(STATUS "this is SOURCE dir "${HELLO_SOURCE_DIR})
    MESSAGE(STATUS "this is PROJECT_SOURCE dir "${PROJECT_SOURCE_DIR})
  • set 用于指定变量 set(key value) ,这里表示 SRC_LIST 变量为 hello.cpp

    1
    
    SET(SRC_LIST hello.cpp)
  • add_executable 表示生成可执行文件,括号中第一个部分表示生成可执行文件的名称。后面跟着项目中所使用的源码文件, 这里 SRC_LIST 是一个变量, 他的值是 hello.cpp

    1
    
    ADD_EXECUTABLE(hello.out ${SRC_LIST})

构建生成 .so 动态库

说白了就是将我们 c 的源代码打包成一个 so 库, 给别人使用的, 现在我们打包成一个 libmath.so 库, 其中的源代码如下

1
2
3
int mul(int num1, int num2){
    return num1 * num2;
}
1
2
3
int sub(int num1, int num2){
    return num1 - num2;
}
1
2
3
int add(int num1, int num2){
    return num1 + num2 + 1;
}
1
2
3
int div(int num1, int num2){
    return num1 / num2;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include<stdio.h>
#include<../include/add.h>
#include<../include/sub.h>
#include<../include/div.h>
#include<../include/mul.h>

int main(int argc, char* argv[]){
    int a = 20;
    int b = 10;
    printf("%d+%d=%d\n",a,b,add(a,b));
    printf("%d-%d=%d\n",a,b,sub(a,b));
    printf("%d/%d=%d\n",a,b,div(a,b));
    return 0;
}

他们的目录结构是这样的

部分头文件都不展示了, 都比较简单, 然后接下来编写我们的最重要的 CMakeLists.txt 文件,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 给工程取一个名字
PROJECT (Math)

MESSAGE(STATUS "this is PROJECT_SOURCE dir "${PROJECT_SOURCE_DIR})

# 指定头文件在哪个目录
INCLUDE_DIRECTORIES (${PROJECT_SOURCE_DIR}/include)

# 指定 src 目录下的所有 .cpp 文件(源文件)
# SRC_LIST 代表 src 目录下的所有源文件 
AUX_SOURCE_DIRECTORY (${PROJECT_SOURCE_DIR}/src SRC_LIST)


# 指定 so 的生成目录 lib
SET (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

MESSAGE (STATUS "src_list : "${SRC_LIST})

# 指定生成动态库 .so  math -> libmath.so  默认生成的是静态库 
ADD_LIBRARY (math SHARED ${SRC_LIST})

这里主要的步骤就是

  1. 添加头文件

    1
    
    INCLUDE_DIRECTORIES (${PROJECT_SOURCE_DIR}/include)
  2. 添加源代码文件

    1
    2
    3
    
    # 指定 src 目录下的所有 .cpp 文件(源文件)
    # SRC_LIST 代表 src 目录下的所有源文件 
    AUX_SOURCE_DIRECTORY (${PROJECT_SOURCE_DIR}/src SRC_LIST)
  3. 指定生成动态库的输出目录

    1
    
    SET (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
  4. 构建 so 库, ADD_LIBRARY 关键字表示构建链接库,第一参是名称;第二参在 SHARED 表示构建 动态链接库, 不配置的话, 默认是 .a 的静态库 ;第三参是源文件列表

    1
    
    ADD_LIBRARY (math SHARED ${SRC_LIST})

ps 到了这里,出了一个小插曲,之前都是在 windows 下构建的, 这一步我们需要构建出 so 库, so 库只能在 Linux 的环境下构建, 所以被迫切换在 Linux 环境中, 我这里是用的是 windows 中的 WSL 子系统, 安装步骤如下

  1. 先去微软商店安装 Ubuntu 系统, 然后打开, 或者在 windows 中, 输入 wsl -u root 进入

  2. 然后安装 cmake 和 gcc , 输入如下命令

    1
    2
    
    apt install cmake
    sudo apt-get install build-essential

安装好了, 输入如下命令, 可以看看有没有安装成功

1
2
3
cmake --version
gcc --version
g++ --version

接着使用 cd /mnt/windows的磁盘路径, 可以访问 windows 的磁盘目录, 然后就可以进行编译了

可以看到, 这样就成功编译了我们想要的 so 库了

链接外部动态库与头文件

在上一个步骤中, 我们已经成功编译出来了 so 库, 接下来, 我们来使用一下这个 so 库, 其实这步骤, 你可以想象成, 你编译了一个 ffmpeng 的 so 库, 现在你要在你的项目链接去是用它, 照例还是编写 CMakeLists

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
PROJECT (HELLO)

# 指定头文件在哪个目录
INCLUDE_DIRECTORIES (${PROJECT_SOURCE_DIR}/include)

# 编译是需要链接 lib 目录下的 libmath.so
# 指定 so 在哪个目录下
LINK_DIRECTORIES (${PROJECT_SOURCE_DIR}/lib)

# 生成可执行文件
ADD_EXECUTABLE (hello hello.cpp)

# 为 hello 添加编译链接库
TARGET_LINK_LIBRARIES (hello math)

集成一个三方的动态链接库, 一共 3 件事

  1. include_directories 搜索引入头文件
  2. link_libraries 搜索对应的链接库
  3. target_link_libraries 对库进行链接,注意名称,这里的库名是 libmath.so ,指定的名称是 math, 如果有多个, 这里可以添加多个

其中, hello.cpp 的代码还是一致的, 可以发现我们这里只是引入了头文件, 源码的细节已经被我们隐藏在 so库, 照例还是 cmake make 等编译

可以看到,运行成功,so 库的使用没有问题,顺带说一句,这个 so 库只是在 Linux 中编译出来的,它只能在 Linux 中使用,如果想要在 Android 中使用,必须交叉编译了,关于如何交叉编译,敬请期待下一篇文章

参考: Android NDK 开发 | CMake 使用手册 - 初见篇 - 掘金