Android NDK(三)Cmake

CMake(跨平台的Make)是一个开源的、跨平台的构建系统。在 Android 中,我们使用 CMake 来构建 C/C++ 代码。这篇文章就将介绍 Cmake 的使用。

单项目的构建

如上图所示,C++ 构建需要经过预处理、编译、汇编、链接等步骤。Cmake 就是用来管理这些步骤的执行,相关的配置都放在 CMakeLists.txt 文件中。一个最简单的 CMakeLists.txt 配置示例如下:

cmake 复制代码
# 指定运行此 CMakeLists.txt 文件所需的 CMake 最低版本为 3.22.1。
cmake_minimum_required(VERSION 3.22.1)

# 定义当前项目的名称为 "hello-jni"。
# 同时 CMake 会创建如 ${PROJECT_NAME} 这样的变量供后续配置使用。
project("hello-jni")

# add_library 指令用于将指定的源文件编译成一个库文件。
# 第一个参数 "hello-jni" 是生成的库文件的名称
# 第二个参数 SHARED" 表明生成的库类型为共享库(动态链接库),静态库则使用 STATIC 关键字
# 第三个参数 "hello-jni.cpp" 是用于编译库文件的源文件,即把这个文件编译成共享库。
add_library(hello-jni SHARED hello-jni.cpp)

# target_link_libraries 指令用于将目标库(或可执行文件)与其他库进行链接。
target_link_libraries(hello-jni #  "hello-jni" 是要进行链接操作的目标库。
                      android # android 是 Android NDK 提供的系统库
                      log) # log 也是 Android NDK 提供的系统库,用于在 Android 平台上进行日志输出

从示例可以看出,我们可以通过在 CMakeLists.txt 中配置参数,来实现C++构建的功能。

Android NDK 提供的系统库,可以在 原生 API 中找到。需要注意 libandroidliblogandroidlog 是指向相同的库,lib 只是 Linux 中的通用前缀。

多文件编译

当源文件很多时,有三种方法来设置需要编译的源文件的位置。

  • 方案一:一一列举
scss 复制代码
add_library(echo
  SHARED
    audio_main.cpp
    audio_player.cpp
    audio_recorder.cpp
    audio_effect.cpp
    audio_common.cpp
    debug_utils.cpp)
  • 方案二:使用 set 设置变量
scss 复制代码
set(SOURCES audio_main.cpp
    audio_player.cpp
    audio_recorder.cpp
    audio_effect.cpp
    audio_common.cpp
    debug_utils.cpp)
add_library(mylib ${SOURCES})
  • 方案三:设置目录
scss 复制代码
file(GLOB SOURCES "src/*.cpp")
add_library(mylib ${SOURCES})

引用了其他库的头文件

如果你的库引用了其他库的头文件,比如#include "utils/native_debug.h",那么你需要使用 target_include_directories 命令来指定这些头文件的位置。代码示例如下:

cmake 复制代码
add_library(hello-jni SHARED hello-jni.cpp)
# 指定 hello-jni 引用头文件的位置
target_include_directories(hello-jni PRIVATE ${CMAKE_SOURCE_DIR}/path/to/include)
  • PRIVATE 和 PUBLIC

PRIVATE 表示这些头文件仅用于 hello-libs 库的编译,不会传递给链接 hello-libs 的其他目标。而 PUBLIC 关键字,意味着这些头文件搜索路径不仅对当前目标的编译有效,还会传递给所有链接该目标的其他目标。

  • CMAKE_SOURCE_DIR

CMAKE_SOURCE_DIR 是预定义变量,代表最顶层的 CMakeLists.txt 文件所在的目录路径。而 ${} 语法则是获取该变量的值。除了这个预定义变量外,常用的预定义变量有:

objectivec 复制代码
CMAKE_CURRENT_SOURCE_DIR 表示当前正在处理的 CMakeLists.txt 文件所在的目录路径

include_directories 也是设置头文件的位置,不同的是 include_directories 设置的是全局的;而 target_include_directories 需要指定库,比如这里是 hello-jni

导入共享库

如果要导入已经编译好的静态库或者动态库,需要在 add_library 使用 IMPORTED。同时需要使用 set_target_properties 来设置导入库的文件位置。

bash 复制代码
// 设置 distribution_DIR 变量的值
set(distribution_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../distribution)

# 声明要导入一个静态库,库的逻辑名称为 lib_gmath。
# 静态库在编译时会被完整地链接到可执行文件或其他库中。
add_library(lib_gmath STATIC IMPORTED)

# 设置导入的静态库 lib_gmath 的实际文件位置。
set_target_properties(lib_gmath PROPERTIES IMPORTED_LOCATION
    ${distribution_DIR}/gmath/lib/${ANDROID_ABI}/libgmath.a)

ANDROID_ABI 变量的值由 Gradle 在运行 CMake 命令时自动传入。变量的值就是当前正在构建的 ABI 类型。我们可以根据不同的 ANDROID_ABI 值进行不同的处理,例如添加不同的编译选项,链接不同的预编译的库等等。

查找库

scss 复制代码
find_library(
    third-party-lib
    third-party
    PATHS ${CMAKE_SOURCE_DIR}/src/main/cpp/third_party/lib
)

target_link_libraries(
    native-lib
    ${third-party-lib}
)

find_library 命令是用来寻找库的,语法是 find_library(variable_to_store_path library_name 路径),它会在设置的路径(如果没有设置,则使用预定义的路径)下查找名为 library_name 的库,然后将找到的库的路径存储在 variable_to_store_path 变量中。

链接顺序

bash 复制代码
target_link_libraries(hello-jni #  "hello-jni" 是要进行链接操作的目标库。
                      android # android 是 Android NDK 提供的系统库
                      log) # log 也是 Android NDK 提供的系统库,用于在 Android 平台上进行日志输出

由于 hello-jni 依赖 androidlog 库,因此 hello-jni 应该放在 androidlog 库前面,而不是后面。注意在 target_link_libraries 方法中这个依赖的顺序不能错误。

Cmake日志

Cmake 的日志格式如下:

cmake 复制代码
message([<mode>] "message text")

其中 <mode> 是可选参数,用于指定消息的类型,代码示例如下:

cmake 复制代码
message("This is a status message")
# WARNING 用于输出警告信息,通常会在控制台以醒目的方式显示。
message(WARNING "This is a warning message")
# 用于输出致命错误信息,CMake 会在输出该消息后终止配置过程
message(FATAL_ERROR "Required file not found")
# 用于输出作者自定义的警告信息
message(AUTHOR_WARNING "This is an author warning")
# 用于标记某个功能或特性已被弃用。
message(DEPRECATION "This feature is deprecated and will be removed in future versions")

Cmake语法

在 Cmake 中,我们可以使用逻辑判断和控制语句来实现更复杂的构建控制功能。

条件表达式

  • 比较运算符LESS(小于)、GREATER(大于)、EQUAL(等于)、LESS_EQUAL(小于等于)、GREATER_EQUAL(大于等于)。
cmake 复制代码
if(${MY_VARIABLE} LESS 10)
    message("MY_VARIABLE is less than 10")
endif()
  • 逻辑运算符AND(逻辑与)、OR(逻辑或)、NOT(逻辑非)。
cmake 复制代码
if(${MY_VARIABLE} GREATER 5 AND ${MY_VARIABLE} LESS 15)
    message("MY_VARIABLE is between 5 and 15")
endif()
  • 字符串判断STREQUAL(字符串相等)、STRLESS(字符串按字典序小于)、STRGREATER(字符串按字典序大于)。
cmake 复制代码
if(${MY_STRING} STREQUAL "hello")
    message("MY_STRING is equal to 'hello'")
endif()
  • 变量存在性判断:可以直接使用变量名来判断变量是否存在且不为空。
cmake 复制代码
if(MY_VARIABLE)
    message("MY_VARIABLE is defined and not empty")
endif()

if

cmake 复制代码
if(condition)
    # 当 condition 为真时执行的命令
    COMMAND1()
    COMMAND2()
elseif(another_condition)
    # 当 another_condition 为真时执行的命令
    COMMAND3()
else()
    # 当所有前面的条件都为假时执行的命令
    COMMAND4()
endif()

while

cmake 复制代码
while(condition)
    # 循环体命令
    COMMAND()
endwhile()

代码示例如下:

cmake 复制代码
set(COUNT 0)
while(${COUNT} LESS 5)
    message("COUNT is ${COUNT}")
    math(EXPR COUNT "${COUNT} + 1")
endwhile()

foreach

cmake 复制代码
foreach(VAR IN ITEMS item1 item2 ...)
    # 循环体命令
    COMMAND(${VAR})
endforeach()

代码示例如下:

cmake 复制代码
foreach(FILE IN ITEMS file1.txt file2.txt file3.txt)
    message("Processing file: ${FILE}")
endforeach()

编译选项

使用 CMake 除了可以控制编译的流程外,还可以设置编译相关的配置。常见的编译选项有

  • target_compile_features 和 target_compile_options
scss 复制代码
# target_compile_features 可以设置编译特性,这里设置为采用 C++14 标准编译
target_compile_features(your-lib PUBLIC cxx_std_14)

# target_compile_options 命令可以用来添加编译选项,
# 这里添加了一个 -Wall 选项,它会让编译器输出所有类型的警告信息
target_compile_options(your-lib PRIVATE -Wall)

add_compile_options 是为项目中的所有目标(可执行文件、库等)添加编译选项

  • set

使用 set 来设置一些配置选项。

bash 复制代码
set(CMAKE_VERBOSE_MAKEFILE on) # 输出编译和链接信息

# `CMAKE_C_FLAGS` 是 CMake 中用于存储 C 编译器编译选项的变量。
# 此语句将 `-Wall`、`-Werror` 和 `-Wno-unused-function` 这三个编译选项追加到原有
# 的 `CMAKE_C_FLAGS` 变量值后面
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror -Wno-unused-function")

多项目的构建

如下所示,大项目都会分模块处理。对于这种情况,子项目的 CMakeLists.txt 不需要特殊处理。而根目录的 CMakeLists.txt 只需要使用 add_subdirectory 引入子项目就可以了。

objectivec 复制代码
project/
│   CMakeLists.txt
│
├───subproject1
│   │   CMakeLists.txt
│   │   other code files...
│
└───subproject2
    │   CMakeLists.txt
    │   other code files...

代码示例如下:

scss 复制代码
// subproject1 的 CMakeLists.txt
add_library(sub1 sub1.cpp)
target_link_libraries(sub1 ...)

// subproject2 的 CMakeLists.txt
add_library(sub2 sub2.cpp)
target_link_libraries(sub2 ...)

// 根目录的 CMakeLists.txt
add_subdirectory(subproject1)
add_subdirectory(subproject2)
add_executable(MyProject main.cpp)
target_link_libraries(MyProject sub1 sub2)

自定义命令

自定义命令看 CMake 高级特性

参考

相关推荐
鸿蒙布道师6 小时前
鸿蒙NEXT开发Base64工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
jiet_h7 小时前
Android adb 的功能和用法
android·adb
美狐美颜sdk7 小时前
美颜SDK兼容性挑战:如何让美颜滤镜API适配iOS与安卓?
android·深度学习·ios·美颜sdk·第三方美颜sdk·视频美颜sdk
居然是阿宋7 小时前
深入理解 YUV 颜色空间:从原理到 Android 视频渲染
android·音视频
KevinWang_8 小时前
DialogFragment 不适合复用
android
古鸽100869 小时前
Audio Hal 介绍
android
小叶不焦虑10 小时前
关于 Android 系统回收站的实现
android
木西10 小时前
从0到1搭建一个RN应用从开发测试到上架全流程
android·前端·react native
小橙子207711 小时前
一条命令配置移动端(Android / iOS)自动化环境
android·ios·自动化
和煦的春风11 小时前
案例分析 | SurfaceFlinger Binder RT 被降级到CFS
android