Android NDK 开发 | CMake 使用手册 - 初见篇

2022-09-20 10:25:33 浏览数 (1)

0. 前言

之前写过一篇 《[-NDK 导引篇 -] 在NDK开发之前你应知道的东西》 介绍了在进入 NDK 学习之前,如何摆正自己的角色。时隔两年,NDK 系列文章开始填坑,在上一篇 《 NDK 是什么 | FFmpeg 5.0 编译 so 库》 中,介绍了 NDK 的概念,以及其作用。

正所谓,工欲善其事必先利其器,接下来将用 2~3 篇来系统介绍一下 CMakeCMakeLists.txt 的语法。CMake 这块知识是比较独立的,不止是 Android NDK 项目,一般的 C 项目也可以通过 CMake 进行构建。为了更具有一般性,将在 Linux 环境中,通过 C 项目来介绍 CMake 的相关知识,当然这一切也可以用于 Android NDK 项目中。

CMake 本质上是一个 编译工具,其最终目的是方便地生成可执行文件或共享库。编译的过程和项目的配置,由 CMakeLists.txt 文件负责定义。这很像 gradle 构建工具和 build.gradle 文件之间的关系,前者是可执行文件,后者是配置定义。

既然 cmake 是一个工具,在使用它之前,首先要拥有它。一般 linux 环境都会有 CMake , 如果是 Windows 环境,在官网下载 即可。在终端输入如下命令,可以查看版本号,有输出表示环境正常:

代码语言:javascript复制
---> cmake --version
cmake version 3.16.3

1. 初识

使用 Clion 创建的 c 项目,默认通过 CMake 进行构建管理。可以看出其中有两个文件和一个文件夹,main.cpp 是源码文件,CMakeLists.txt 是项目的配置文件。另外说明一点,这里 Clion 工具并不重要,只是起到编辑的作用,只要有 CMake 环境,你用文本编辑器进行书写也可以。

代码语言:javascript复制
cmake_minimum_required(VERSION 3.16)
project(cmake)

set(CMAKE_CXX_STANDARD 17)

add_executable(cmake main.cpp)

另外 cmake-build-debug 文件夹是构建产物,可以在其中执行如下命令来生成。其中 .. 表示 CMakeLists.txt 在当前目录的上级目录。

代码语言:javascript复制
cmake ..

当有了 Makefile 文件,可以使用如下命令来构建可执行文件:

代码语言:javascript复制
make

在命令行执行该文件,可以看到 main.cpp 中输出结果的逻辑被执行。

代码语言:javascript复制
---> ./cmake_test
Hello, World!

其实 IDE 中点击运行按钮,在控制台打印结果,期间就在做这些事。这就是通过 CMake 构建 C 项目最简单的案例。


2. CMakeLists 语法初见

在上面的最简案例中,CMakeLists 内写了四行内容,我们来看一下其中的含义。

  • cmake_minimum_required 表示支持的 cmake 最小版本,这里是 3.16
代码语言:javascript复制
cmake_minimum_required(VERSION 3.16)
  • project 表示项目名称,这里是 cmake_test
代码语言:javascript复制
project(cmake_test)
  • set 用于指定变量 set(key value) ,这里表示 CMAKE_CXX_STANDARD 变量为 17
代码语言:javascript复制
set(CMAKE_CXX_STANDARD 17)
  • add_executable 表示生成可执行文件,括号中第一个部分表示生成可执行文件的名称。后面跟着项目中所使用的源码文件。
代码语言:javascript复制
add_executable(cmake_test main.cpp)

另外, CMakeLists 中的关键字大小写并没有强制的要求,根据个人风格或团队规定统一即可,形式上的东西,不必过于纠结。我个人更倾向于小写,因为看着直接,google 开源的 glog、leveldb 项目采用的是小写字母。


3.配置多文件

之前写过一篇有意思的 C 引文, 《C 趣玩篇 | 从打印开始说起》 , 封装了一个 Facer 类用于打印字符脸。这里刚好拿来当测试文件使用。

如下,将 Facer 的头文件和实现文件放入 src 中,此时在 CMakeLists 里需要指定这些文件,这样在构建时才能找到它们,不至于出错。


4.打印输出与文件夹搜索

这样会出现一个问题,如果源码文件非常多,一一列举会非常复杂。如果能对某个文件夹进行自动搜索包含就好了。 如下测试代码结构如下,有 srcfacer 两个文件夹,一共三个类,六个文件。下面来看一下如何对文件夹内的文件进行统一搜索。

代码语言:javascript复制
├── main.cpp
├── facer
│   ├── Facer.cpp
│   ├── Facer.h
└── src
    ├── A.cpp
    ├── A.h
    ├── B.cpp
    └── B.h

我们先来看个信息输出的语法,通过 message 关键字可以在构建过程中打印信息。STATUS 表示普通信息,第二参是需要输出的信息,如下是输出 PROJECT_SOURCE_DIR 变量的值

代码语言:javascript复制
message(STATUS PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR})

在构建时可以在控制台观察到,PROJECT_SOURCE_DIR 代表的就是当前项目在磁盘的根目录


使用 include_directories 可以搜索头文件进行包含,这样在使用某类时直接使用名称即可,不需要指定相对路径。 aux_source_directory 可以搜索文件夹中的实现文件,并添加到后面的变量中,这里是 SRC_LIST

代码语言:javascript复制
cmake_minimum_required(VERSION 3.16)
project(cmake_test)
set(CMAKE_CXX_STANDARD 17)

# 头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/facer)
include_directories(${PROJECT_SOURCE_DIR}/src)

# 源文件搜索路径
aux_source_directory(${PROJECT_SOURCE_DIR}/src SRC_LIST)
aux_source_directory(${PROJECT_SOURCE_DIR}/facer SRC_LIST)

message(STATUS SRC_LIST: ${SRC_LIST})

add_executable(cmake_test main.cpp ${SRC_LIST})

如下所示,可以打印 SRC_LIST 变量来看一下,其内容是对应文件夹在的 cpp 文件。也就是说在在通过 include_directories 包含头文件之后,add_executable 中只需要记录实现文件即可。


这样 main.cpp 中就可以引入头文件,使用相关的类。这里 AB 比较简单,有一个 print 方法输出信息,这里就不贴了。通过这个小案例,多文件的 CMakeLists 配置方式就介绍地非常清楚了。

代码语言:javascript复制
#include "Facer.h"
#include "A.h"
#include "B.h"

int main() {
    Facer facer;
    facer.printFace();
    A a;
    a.print();
    B b;
    b.print();
    return 0;
}

5. 链接库的构建与集成

Android 的朋友应该对 so 动态链接库并不陌生,windows 的朋友对 dll 动态链接库也不陌生。其实两者本质上是类似的,只是在不同平台构建的产物不同罢了。在日常开发中,很多东西其实并不会从零开始写,而是引入三方库,比如 opencvffmpeg高德地图 等。只要有 so 文件和 头文件 就可以使用在项目中,这样也有利于某些公司在提供一些算法服务的同时,保证源码实现的私密。

那如何根据源码生成链接库呢? 其实在上一篇介绍 ffmpeg 编译的过程,就是将源代码编译为动态链接库的过程。下面来通过一个更简单的例子看一下。比如现在想要把 facer 的源码实现细节屏蔽掉,不想让外界知晓,但又希望 facer 这个库可以为别人服务。这时候就需要将它编译为动态链接库。


现在将 facer 作为一个独立的项目,我们的目标是编译出动态链接库,代码结构如下:

代码语言:javascript复制
├── CMakeLists.txt
├── Facer.cpp
├── Facer.h

下面是 CMakeLists.txt 文件中的配置信息,通过 add_library 关键字表示构建链接库,第一参是名称;第二参在 SHARED 表示构建 动态链接库 ;第三参是源文件列表。

代码语言:javascript复制
cmake_minimum_required(VERSION 3.16)
project(facer)

add_library(facer SHARED Facer.cpp)

接下来在 cmake-build-debug 文件夹中,通过 cmake..make 命令即可构建出 .so 文件,如下所示“”


下面来看一下在项目中如何集成 .so 文件,现在回到测试项目,在其中的创建 includeslibs 文件夹分别盛放 头文件动态链接库文件,这也是第三方库会为你提供的东西。可以看出,目前代码中并没有显示地提供 Facer.cpp ,也就是隐藏了逻辑的实现细节。


集成一个三方的动态链接库,只需在 CMakeLists.txt 中办三件事:

  • include_directories : 搜索引入对应的头文件
  • link_libraries : 搜索对应的链接库
  • target_link_libraries: 对库进行链接,注意名称,这里的库名是 libfacer.so ,指定的名称是 facer
代码语言:javascript复制
cmake_minimum_required(VERSION 3.16)
project(cmake_test)

set(CMAKE_CXX_STANDARD 17)

# 头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/includes/facer)
include_directories(${PROJECT_SOURCE_DIR}/src)

# 源文件搜索路径
aux_source_directory(${PROJECT_SOURCE_DIR}/src SRC_LIST)

# 共享库搜索路径
link_libraries(${PROJECT_SOURCE_DIR}/libs/facer)

add_executable(cmake_test main.cpp ${SRC_LIST})

# 指定链接库名称
target_link_libraries(${PROJECT_NAME} facer)

6. 回首 Android NDK 中的 CMakeLists

Android NDK 中的 CMakeListsC 项目中的并没有任何区别,都是用来构建项目的。如下是一个名为 toly_ndk 初始项目,现在再来回看想必会有不少亲切感。可以看出第五行通过 add_library 关键字将其中的 C 代码构建为 SHARED ,也就是动态链接库。

Android 项目构建过程中,会使用 ndk 通过 CMakeLists 来构建 C 相关的代码,如下可以看出,在构建产物中确实会存在构建的 .so 动态链接库。

我们也可以把构建的 apk 解压看一下,如下 .so 文件会被打包到应用中,放在 lib 文件夹内。


从这里可以感觉到,NDK 开发 本质上就是通过 动态链接库Java 通过 JNI 接口来访问 C 方法的。结合 MainActivity 中需要使用 System.loadLibrary 加载相关库,就能理解我们在 Android 项目中写的 C 代码去向。 而不是感觉这些都是魔法,就像我第一次接触时,就不由感慨。


接下来我们将刚才编译的 libfacer.so 在这里集成一下做个小结。但当使用那个 so 时,放入 arm64-v8a下, 会出现一个如下问题:

原因很简单,因为架构问题,通过 cmakelinux 中构建的 so 文件,是 X86_64 架构的,在 Linux 中可以通过 readelf -h 查看动态链接库的信息:


那么现在问题来了,我们该如何获取各个架构的 so 呢?其实答案很明显,在上一篇中,我们通过 NDK 来编译 ffmpeg 并生成四个架构的 so。那 facer 又何尝不可呢?关于这点,本文不做展开,将在下一篇进行详述。

不过,有个投机取巧的好方式,就是让 AndroidStudio 帮我们构建动态链接库。因为我们前面说过,AndroidStudio 会将 C 源码编译为各平台的 so ,比如下面新建的 facer 项目,在构建产物中就可以 “借鸡生蛋”

在刚才的初始项目中,引入这些 so 即可:


最后我们就可以在 native-lib.cpp 中使用 Facer 类的功能。这其实和引入别的三方库是类似的,现在再回首之前对 ffmpegopencv 的集成,应该会有更多体悟。

代码语言:javascript复制
--->[cpp/native-lib.cpp]----
#include <jni.h>
#include <string>
#include "Facer.h"

extern "C" JNIEXPORT jstring JNICALL
Java_com_toly1994_toly_1ndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    Facer facer;
    std::string hello = facer.getFace();
    return env->NewStringUTF(hello.c_str());
}

然后在手机上就能调用获取脸的方法,展示一个变了形的脸,看来控制台和手机显示还是很有差距的。不过这只是案例而已,不用太在意这些细节。


本文介绍了 CMakeLists.txt 的一些简单语法,要点是如何管理多文件,以及构建和集成链接库。知道在哪写的代码,写的代码会跑到哪去,其实是很重要的,编程中不需要 “魔法”,一切的神奇都应是理所应当。那本文就到这里,谢谢观看 ~

0 人点赞