Android CMake

目的

向您的项目添加 C 和 C++ 代码。提供 CMake 或 ndk-build 脚本文件的路径以配置 Gradle。Gradle 使用构建脚本将源代码导入您的 Android Studio 项目并将原生库(SO 文件)打包到 APK 中。

概述

CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平摊的安装编译过程。它能够输出各种各样的makefile或者project文件,能测试编译器所支持的C++特性,类似UNIX下的automake。

配置CMake 3.6.0 或 3.10.2

CMake 3.6.0 或 3.10.2

SDK 管理器包含 CMake 的 3.6.0 派生版本和版本 3.10.2。未在 build.gradle 中设置特定 CMake 版本的项目均使用 CMake 3.10.2 进行构建。要使用之前包含的版本,请在模块的 build.gradle 文件中指定 CMake 版本 3.6.0:

1
2
3
4
5
6
7
8
9
android {
...
externalNativeBuild {
cmake {
...
version "3.6.0"
}
}
}

您可以指定此 CMake 版本作为最低版本,只需在 build.gradle 条目的末尾添加一个“+”即可,例如 3.10.2+。不过,这并非最佳做法。

使用自定义 CMake 版本

如果您想使用 SDK 管理器中未包含的 CMake 版本 3.7 或更高版本,请按以下步骤操作:

1.从官方 CMake 网站下载并安装 CMake 3.7 或更高版本
2.指定您想要让 Gradle 在模块的 build.gradle 文件中使用的 CMake 版本:

1
2
3
4
5
6
7
8
9
android {
...
externalNativeBuild {
cmake {
...
version "cmake-version"
}
}
}

3.将 CMake 的安装路径添加到 PATH 环境变量,或将其添加到项目的 local.properties 文件中,具体步骤如下所示。如果 Gradle 找不到您在 build.gradle 文件中指定的 CMake 版本,您便会遇到构建错误。
# If you set this property, Gradle no longer uses PATH to find CMake.
cmake.dir=”path-to-cmake”

4.如果您尚未在工作站上安装 Ninja 构建系统,请访问 Ninja 官方网站,然后下载并安装适用于您的操作系统的最新版 Ninja。此外,请务必将 Ninja 的安装路径也添加到 PATH 环境变量。

创建支持 C/C++ 的新项目

创建支持原生代码的新项目的步骤与创建任何其他 Android Studio 项目的步骤相似,但前者还需要执行一个额外的步骤:

1.在向导的 Choose your project 部分中,选择 Native C++ 项目类型。
2.点击 Next。
3.填写向导下一部分中的所有其他字段。
4.点击 Next。
5.在向导的 Customize C++ Support 部分中,您可以使用 C++ Standard 字段来自定义项目。使用下拉列表选择您想要使用哪种 C++ 标准化。选择 Toolchain Default 可使用默认的 CMake 设置。
6.点击 Finish。

原有项目支持 C/C++

配置 CMake

CMake 构建脚本是一个纯文本文件,您必须将其命名为 CMakeLists.txt,并在其中包含 CMake 构建您的 C/C++ 库时需要使用的命令。如果您的原生源代码文件还没有 CMake 构建脚本,您需要自行创建一个,并在其中包含适当的 CMake 命令。

本部分将介绍您应该在构建脚本中包含哪些基本命令,以便指示 CMake 在创建原生库时使用哪些源代码文件。如需了解详情,请参阅介绍 CMake 命令的官方文档

在配置新的 CMake 构建脚本后,您需要配置 Gradle 以将 CMake 项目作为构建依赖项包含在内,从而让 Gradle 构建原生库,并将其与应用的 APK 打包在一起。

  • 注意:如果项目使用的是 ndk-build,您不需要创建 CMake 构建脚本。您只需配置 Gradle 以包含现有的原生库项目即可;为此,您需要提供 Android.mk 文件的路径。

创建 CMake 构建脚本

要创建一个可以用作 CMake 构建脚本的纯文本文件,请按以下步骤操作:

1.从 IDE 的左侧打开 Project 窗格,然后从下拉菜单中选择 Project 视图。
2.右键点击 your-module 的根目录,然后依次选择 New > File。

  • 注意:您可以在所需的任何位置创建构建脚本。不过,在配置构建脚本时,原生源代码文件和库的路径将与构建脚本的位置相关。

3.输入“CMakeLists.txt”作为文件名,然后点击 OK。
现在,您可以通过添加 CMake 命令来配置您的构建脚本。要指示 CMake 根据原生源代码创建原生库,请向您的构建脚本添加 cmake_minimum_required() 和 add_library() 命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.

cmake_minimum_required(VERSION 3.4.1)

# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add_library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.

add_library( # Specifies the name of the library.
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
  • 提示:与指示 CMake 根据源代码文件创建原生库的方式一样,您可以使用 add_executable() 命令指示 CMake 改为根据这些源代码文件来创建可执行文件。不过,根据原生源代码文件构建可执行文件是可选操作,构建原生库以将其打包到 APK 中即可满足大多数项目的要求。

在使用 add_library() 向 CMake 构建脚本添加源代码文件或库时,Android Studio 还会在您同步项目后在 Project 视图中显示相关的头文件。不过,为了让 CMake 能够在编译时找到头文件,您需要向 CMake 构建脚本添加 include_directories() 命令,并指定头文件的路径:

1
2
3
4
add_library(...)

# Specifies a path to native header files.
include_directories(src/main/cpp/include/)

CMake 使用以下规范来为库文件命名:

liblibrary-name.so
例如,如果您在构建脚本中指定“native-lib”作为共享库的名称,CMake 就会创建一个名为 libnative-lib.so 的文件。不过,在 Java 或 Kotlin 代码中加载此库时,请使用您在 CMake 构建脚本中指定的名称:

1
2
3
static {
System.loadLibrary("native-lib");
}
  • 注意:如果重命名或移除了 CMake 构建脚本中的库,您需要在 Gradle 实施相关更改或从 APK 中移除旧版库之前清理您的项目。要清理项目,请在菜单栏中依次选择 Build > Clean Project。

Android Studio 会自动向 Project 窗格中的 cpp 群组添加源代码文件和头文件。通过使用多个 add_library() 命令,您可以为 CMake 定义要根据其他源代码文件构建的更多库。

添加 NDK API

Android NDK 提供了一套您可能会觉得非常实用的原生 API 和库。通过在项目的 CMakeLists.txt 脚本文件中包含 NDK 库,您可以使用其中任何 API。

Android 平台上已存在预构建的 NDK 库,因此您无需构建它们或将它们打包到 APK 中。由于这些 NDK 库已位于 CMake 搜索路径中,因此您甚至无需指定本地安装的 NDK 库的位置,您只需为 CMake 提供您想要使用的库的名称,并将其与您自己的原生库相关联即可。

向 CMake 构建脚本添加 find_library() 命令以找到 NDK 库并将其路径存储为一个变量。您可以使用此变量在构建脚本的其他部分引用 NDK 库。以下示例可以找到 Android 专有的日志支持库,并会将其路径存储在 log-lib 中:

1
2
3
4
5
6
7
find_library( # Defines the name of the path variable that stores the
# location of the NDK library.
log-lib

# Specifies the name of the NDK library that
# CMake needs to locate.
log )

为了让您的原生库能够调用 log 库中的函数,您需要使用 CMake 构建脚本中的 target_link_libraries() 命令来关联这些库:

1
2
3
4
5
6
7
8
find_library(...)

# Links your native library against one or more other native libraries.
target_link_libraries( # Specifies the target library.
native-lib

# Links the log library to the target library.
${log-lib} )

NDK 还以源代码的形式包含一些库,您将需要构建这些代码并将其关联到您的原生库。您可以使用 CMake 构建脚本中的 add_library() 命令将源代码编译到原生库中。如需提供本地 NDK 库的路径,您可以使用 Android Studio 自动为您定义的 ANDROID_NDK 路径变量。

以下命令告诉 CMake 要构建 android_native_app_glue.c(负责管理 NativeActivity 生命周期事件和触摸输入),并将其链接到静态库 native-lib 中:

1
2
3
4
5
6
add_library( app-glue
STATIC
${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c )

# You need to link static libraries against your shared native library.
target_link_libraries( native-lib app-glue ${log-lib} )

添加其他预构建库

添加预构建库的步骤与为 CMake 指定其他要构建的原生库的步骤相似。不过,由于库已构建,因此您需要使用 IMPORTED 标记告诉 CMake 您只想要将此库导入到您的项目中。

1
2
3
add_library( imported-lib
SHARED
IMPORTED )

然后,您需要使用 set_target_properties() 命令指定库的路径,具体步骤如下所示。

某些库会针对特定的 CPU 架构或应用二进制接口 (ABI) 提供单独的软件包,并将其整理到单独的目录中。此方法既有助于库充分利用特定的 CPU 架构,又能让您只使用所需的库版本。要向 CMake 构建脚本添加库的多个 ABI 版本,而不必为库的每个版本编写多个命令,您可以使用 ANDROID_ABI 路径变量。此变量使用的是 NDK 支持的一组默认 ABI,或者您手动配置 Gradle 以使用的一组经过过滤的 ABI。例如:

1
2
3
4
5
6
7
8
9
add_library(...)
set_target_properties( # Specifies the target library.
imported-lib

# Specifies the parameter you want to define.
PROPERTIES IMPORTED_LOCATION

# Provides the path to the library you want to import.
imported-lib/src/${ANDROID_ABI}/libimported-lib.so )

为了让 CMake 能够在编译时定位您的头文件,您需要使用 include_directories() 命令并包含相应头文件的路径:

1
include_directories( imported-lib/include/ )
  • 注意:如果您想要打包不属于构建时依赖项的预构建库(例如在添加属于 imported-lib 依赖项的预构建库时),则无需按以下说明操作来关联库。

如需将预构建库关联到您自己的原生库,请将其添加到 CMake 构建脚本的 target_link_libraries() 命令中:

1
target_link_libraries( native-lib imported-lib app-glue ${log-lib} )

如需将预构建库打包到 APK 中,您需要使用 sourceSets 块手动配置 Gradle 以包含 .so 文件的路径。构建 APK 后,您可以使用 APK 分析器验证 Gradle 会将哪些库打包到您的 APK 中。

包含其他 CMake 项目

如果想要构建多个 CMake 项目并在 Android 项目中包含它们的输出,您可以使用一个 CMakeLists.txt 文件(即您关联到 Gradle 的那个文件)作为顶级 CMake 构建脚本,并添加其他 CMake 项目作为此构建脚本的依赖项。以下顶级 CMake 构建脚本会使用 add_subdirectory() 命令将另一个 CMakeLists.txt 文件指定为构建依赖项,然后关联其输出,就像处理任何其他预构建库一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Sets lib_src_DIR to the path of the target CMake project.
set( lib_src_DIR ../gmath )

# Sets lib_build_DIR to the path of the desired output directory.
set( lib_build_DIR ../gmath/outputs )
file(MAKE_DIRECTORY ${lib_build_DIR})

# Adds the CMakeLists.txt file located in the specified directory
# as a build dependency.
add_subdirectory( # Specifies the directory of the CMakeLists.txt file.
${lib_src_DIR}

# Specifies the directory for the build outputs.
${lib_build_DIR} )

# Adds the output of the additional CMake build as a prebuilt static
# library and names it lib_gmath.
add_library( lib_gmath STATIC IMPORTED )
set_target_properties( lib_gmath PROPERTIES IMPORTED_LOCATION
${lib_build_DIR}/${ANDROID_ABI}/lib_gmath.a )
include_directories( ${lib_src_DIR}/include )

# Links the top-level CMake build output against lib_gmath.
target_link_libraries( native-lib ... lib_gmath )

关联Gradle

手动配置 Gradle

要手动配置 Gradle 以关联到您的原生库,您需要将 externalNativeBuild 块添加到模块级 build.gradle 文件中,并使用 cmake 或 ndkBuild 块对其进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

android {
...
defaultConfig {...}
buildTypes {...}

// Encapsulates your external native build configurations.
externalNativeBuild {

// Encapsulates your CMake build configurations.
cmake {

// Provides a relative path to your CMake build script.
path "CMakeLists.txt"
}
}
}
  • 注意:如果您要将 Gradle 关联到现有的 ndk-build 项目,请使用 ndkBuild 块(而不是 cmake 块),并提供指向 Android.mk 文件的相对路径。如果 Application.mk 文件与您的 Android.mk 文件位于同一目录下,Gradle 也会包含此文件。

指定可选配置

您可以在模块级 build.gradle 文件的 defaultConfig 块中配置另一个 externalNativeBuild 块,为 CMake 或 ndk-build 指定可选参数和标记。与 defaultConfig 块中的其他属性类似,您也可以在构建配置中为每种产品特性重写这些属性。

例如,如果您的 CMake 或 ndk-build 项目定义多个原生库和可执行文件,您可以使用 targets 属性为指定产品特性构建和打包其中的部分工件。以下代码示例说明了您可以配置的部分属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

android {
...
defaultConfig {
...
// This block is different from the one you use to link Gradle
// to your CMake or ndk-build script.
externalNativeBuild {

// For ndk-build, instead use the ndkBuild block.
cmake {

// Passes optional arguments to CMake.
arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang"

// Sets a flag to enable format macro constants for the C compiler.
cFlags "-D__STDC_FORMAT_MACROS"

// Sets optional flags for the C++ compiler.
cppFlags "-fexceptions", "-frtti"
}
}
}

buildTypes {...}

productFlavors {
...
demo {
...
externalNativeBuild {
cmake {
...
// Specifies which native libraries or executables to build and package
// for this product flavor. The following tells Gradle to build only the
// "native-lib-demo" and "my-executible-demo" outputs from the linked
// CMake project. If you don't configure this property, Gradle builds all
// executables and shared object libraries that you define in your CMake
// (or ndk-build) project. However, by default, Gradle packages only the
// shared libraries in your APK.
targets "native-lib-demo",
// You need to specify this executable and its sources in your CMakeLists.txt
// using the add_executable() command. However, building executables from your
// native sources is optional, and building native libraries to package into
// your APK satisfies most project requirements.
"my-executible-demo"
}
}
}

paid {
...
externalNativeBuild {
cmake {
...
targets "native-lib-paid",
"my-executible-paid"
}
}
}
}

// Use this block to link Gradle to your CMake or ndk-build script.
externalNativeBuild {
cmake {...}
// or ndkBuild {...}
}
}

如需详细了解如何配置产品变种和构建变体,请参阅配置构建变体。如需了解您可以使用 arguments 属性为 CMake 配置的变量列表,请参阅使用 CMake 变量。

添加预构建的原生库

如果您希望 Gradle 将预构建的原生库打包到您的 APK 中,请修改默认的源代码集配置,以添加预构建 .so 文件所在的目录,如下所示。请注意,若要添加关联到 Gradle 的 CMake 构建脚本的工件,则无需执行此操作。

1
2
3
4
5
6
7
8
android {
...
sourceSets {
main {
jniLibs.srcDirs 'imported-lib/src/', 'more-imported-libs/src/'
}
}
}

指定 ABI

默认情况下,Gradle 会针对 NDK 支持的应用二进制接口 (ABI) 将您的原生库构建到单独的 .so 文件中,并将这些文件全部打包到您的 APK 中。如果您希望 Gradle 仅构建和打包原生库的部分 ABI 配置,则可以在模块级 build.gradle 文件中使用 ndk.abiFilters 标记指定这些配置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {...}
// or ndkBuild {...}
}

// Similar to other properties in the defaultConfig block,
// you can configure the ndk block for each product flavor
// in your build configuration.
ndk {
// Specifies the ABI configurations of your native
// libraries Gradle should build and package with your APK.
abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a',
'arm64-v8a'
}
}
buildTypes {...}
externalNativeBuild {...}
}

在大多数情况下,您只需要在 ndk 块中指定 abiFilters(如上所示),因为它会指示 Gradle 构建和打包原生库的这些版本。但是,如果您想控制 Gradle 应当构建的配置,而不依赖于您希望其打包到 APK 中的配置,请在 defaultConfig.externalNativeBuild.cmake 块(或 defaultConfig.externalNativeBuild.ndkBuild 块)中配置另一个 abiFilters 标记。Gradle 会构建这些 ABI 配置,但只会打包您在 defaultConfig.ndk 块中指定的配置。

为了进一步降低 APK 的大小,请考虑基于 ABI 配置多个 APK,而不是创建一个包含原生库所有版本的大型 APK。Gradle 会为您想要支持的每个 ABI 创建单独的 APK,并且仅打包每个 ABI 需要的文件。如果您为每个 ABI 配置多个 APK,而不像上面的代码示例中所示的那样指定 abiFilters 标记,则 Gradle 会为您的原生库构建所有受支持的 ABI 版本,但是仅打包您在多 APK 配置中指定的版本。为避免构建不想要的原生库版本,请为 abiFilters 标记和“一个 ABI 多个 APK”配置提供相同的 ABI 列表。

异常问题


参考资料

developer.android.com