0. 前言
之前写过一篇 《[-NDK 导引篇 -] 在NDK开发之前你应知道的东西》 介绍了在进入 NDK
学习之前,如何摆正自己的角色。时隔两年,NDK
系列文章开始填坑,在上一篇 《 NDK 是什么 | FFmpeg 5.0 编译 so 库》 中,介绍了 NDK
的概念,以及其作用。
正所谓,工欲善其事必先利其器,接下来将用 2~3
篇来系统介绍一下 CMake
及 CMakeLists.txt
的语法。CMake
这块知识是比较独立的,不止是 Android NDK
项目,一般的 C
项目也可以通过 CMake
进行构建。为了更具有一般性,将在 Linux
环境中,通过 C
项目来介绍 CMake
的相关知识,当然这一切也可以用于 Android NDK
项目中。
CMake
本质上是一个 编译工具
,其最终目的是方便地生成可执行文件或共享库。编译的过程和项目的配置,由 CMakeLists.txt
文件负责定义。这很像 gradle
构建工具和 build.gradle
文件之间的关系,前者是可执行文件,后者是配置定义。
既然 cmake
是一个工具,在使用它之前,首先要拥有它。一般 linux
环境都会有 CMake
, 如果是 Windows
环境,在官网下载 即可。在终端输入如下命令,可以查看版本号,有输出表示环境正常:
---> cmake --version
cmake version 3.16.3
1. 初识
使用 Clion
创建的 c
项目,默认通过 CMake
进行构建管理。可以看出其中有两个文件和一个文件夹,main.cpp
是源码文件,CMakeLists.txt
是项目的配置文件。另外说明一点,这里 Clion
工具并不重要,只是起到编辑的作用,只要有 CMake
环境,你用文本编辑器进行书写也可以。
cmake_minimum_required(VERSION 3.16)
project(cmake)
set(CMAKE_CXX_STANDARD 17)
add_executable(cmake main.cpp)
另外 cmake-build-debug
文件夹是构建产物,可以在其中执行如下命令来生成。其中 ..
表示 CMakeLists.txt
在当前目录的上级目录。
cmake ..
当有了 Makefile
文件,可以使用如下命令来构建可执行文件:
make
在命令行执行该文件,可以看到 main.cpp
中输出结果的逻辑被执行。
---> ./cmake_test
Hello, World!
其实 IDE
中点击运行按钮,在控制台打印结果,期间就在做这些事。这就是通过 CMake
构建 C
项目最简单的案例。
2. CMakeLists 语法初见
在上面的最简案例中,CMakeLists
内写了四行内容,我们来看一下其中的含义。
cmake_minimum_required
表示支持的cmake
最小版本,这里是3.16
。
cmake_minimum_required(VERSION 3.16)
project
表示项目名称,这里是cmake_test
。
project(cmake_test)
set
用于指定变量set(key value)
,这里表示CMAKE_CXX_STANDARD
变量为17
set(CMAKE_CXX_STANDARD 17)
add_executable
表示生成可执行文件,括号中第一个部分表示生成可执行文件的名称。后面跟着项目中所使用的源码文件。
add_executable(cmake_test main.cpp)
另外, CMakeLists
中的关键字大小写并没有强制的要求,根据个人风格或团队规定统一即可,形式上的东西,不必过于纠结。我个人更倾向于小写,因为看着直接,google 开源的 glog、leveldb 项目采用的是小写字母。
3.配置多文件
之前写过一篇有意思的 C
引文, 《C 趣玩篇 | 从打印开始说起》 , 封装了一个 Facer
类用于打印字符脸。这里刚好拿来当测试文件使用。
如下,将 Facer
的头文件和实现文件放入 src
中,此时在 CMakeLists
里需要指定这些文件,这样在构建时才能找到它们,不至于出错。
4.打印输出与文件夹搜索
这样会出现一个问题,如果源码文件非常多,一一列举会非常复杂。如果能对某个文件夹进行自动搜索包含就好了。 如下测试代码结构如下,有 src
和 facer
两个文件夹,一共三个类,六个文件。下面来看一下如何对文件夹内的文件进行统一搜索。
├── main.cpp
├── facer
│ ├── Facer.cpp
│ ├── Facer.h
└── src
├── A.cpp
├── A.h
├── B.cpp
└── B.h
我们先来看个信息输出的语法,通过 message
关键字可以在构建过程中打印信息。STATUS
表示普通信息,第二参是需要输出的信息,如下是输出 PROJECT_SOURCE_DIR
变量的值
message(STATUS PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR})
在构建时可以在控制台观察到,PROJECT_SOURCE_DIR
代表的就是当前项目在磁盘的根目录
使用 include_directories
可以搜索头文件进行包含,这样在使用某类时直接使用名称即可,不需要指定相对路径。 aux_source_directory
可以搜索文件夹中的实现文件,并添加到后面的变量中,这里是 SRC_LIST
。
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
中就可以引入头文件,使用相关的类。这里 A
和 B
比较简单,有一个 print
方法输出信息,这里就不贴了。通过这个小案例,多文件的 CMakeLists
配置方式就介绍地非常清楚了。
#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
动态链接库也不陌生。其实两者本质上是类似的,只是在不同平台构建的产物不同罢了。在日常开发中,很多东西其实并不会从零开始写,而是引入三方库,比如 opencv
、ffmpeg
、高德地图
等。只要有 so
文件和 头文件
就可以使用在项目中,这样也有利于某些公司在提供一些算法服务的同时,保证源码实现的私密。
那如何根据源码生成链接库呢? 其实在上一篇介绍 ffmpeg
编译的过程,就是将源代码编译为动态链接库的过程。下面来通过一个更简单的例子看一下。比如现在想要把 facer
的源码实现细节屏蔽掉,不想让外界知晓,但又希望 facer
这个库可以为别人服务。这时候就需要将它编译为动态链接库。
现在将 facer
作为一个独立的项目,我们的目标是编译出动态链接库,代码结构如下:
├── CMakeLists.txt
├── Facer.cpp
├── Facer.h
下面是 CMakeLists.txt
文件中的配置信息,通过 add_library
关键字表示构建链接库,第一参是名称;第二参在 SHARED
表示构建 动态链接库
;第三参是源文件列表。
cmake_minimum_required(VERSION 3.16)
project(facer)
add_library(facer SHARED Facer.cpp)
接下来在 cmake-build-debug
文件夹中,通过 cmake..
和 make
命令即可构建出 .so
文件,如下所示“”
下面来看一下在项目中如何集成 .so
文件,现在回到测试项目,在其中的创建 includes
和 libs
文件夹分别盛放 头文件
和 动态链接库文件
,这也是第三方库会为你提供的东西。可以看出,目前代码中并没有显示地提供 Facer.cpp
,也就是隐藏了逻辑的实现细节。
集成一个三方的动态链接库,只需在 CMakeLists.txt
中办三件事:
include_directories
: 搜索引入对应的头文件link_libraries
: 搜索对应的链接库target_link_libraries
: 对库进行链接,注意名称,这里的库名是libfacer.so
,指定的名称是facer
。
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
中的 CMakeLists
和 C
项目中的并没有任何区别,都是用来构建项目的。如下是一个名为 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
下, 会出现一个如下问题:
原因很简单,因为架构问题,通过 cmake
在 linux
中构建的 so
文件,是 X86_64
架构的,在 Linux
中可以通过 readelf -h
查看动态链接库的信息:
那么现在问题来了,我们该如何获取各个架构的 so
呢?其实答案很明显,在上一篇中,我们通过 NDK
来编译 ffmpeg
并生成四个架构的 so
。那 facer
又何尝不可呢?关于这点,本文不做展开,将在下一篇进行详述。
不过,有个投机取巧的好方式,就是让 AndroidStudio
帮我们构建动态链接库。因为我们前面说过,AndroidStudio
会将 C
源码编译为各平台的 so
,比如下面新建的 facer
项目,在构建产物中就可以 “借鸡生蛋”
。
在刚才的初始项目中,引入这些 so
即可:
最后我们就可以在 native-lib.cpp
中使用 Facer
类的功能。这其实和引入别的三方库是类似的,现在再回首之前对 ffmpeg
、opencv
的集成,应该会有更多体悟。
--->[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
的一些简单语法,要点是如何管理多文件,以及构建和集成链接库。知道在哪写的代码,写的代码会跑到哪去,其实是很重要的,编程中不需要 “魔法”
,一切的神奇都应是理所应当。那本文就到这里,谢谢观看 ~