CMAKE与Android NDK开发

在Android Studio 2.2开始,正式支持cmake编译,在与android studio结合之前,cmake就已经作为一个广泛使用的构建系统,应用在许多项目中。通过cmake与ndk,我们可以将c/c++源码编译成动/静态库、可执行程序等,非常的方便。

认识CMake

在使用cmake之前,我们需要先了解一下cmake,最直接的了解方式是官网。当然还有tutorial最好需要看一下,这样你就能大概理解cmake的一些用法。如果你还不了解c/c++的编译过程,请自行百度学习,不在本文叙述范围内。

CMake的基本操作

在看过官网的资料和tutorial之后,我们需要动手实操一下,如果使用cmake进行编译,这样我们才能更好的掌握cmake的一些用法。对于我们后续cmake与ndk的结合有莫大的帮助。我们这里通过clion作为ide进行demo的学习。

创建可执行程序

# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)

# 工程名字 工程语言
project(cmakedemo C)

# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)

# 将main.c文件加入到可执行程序cmakedemo中
add_executable(cmakedemo main.c)

add_executable第一个参数是生成可执行程序名称,第二个参数是源码文件,如果有多个源码文件,依次加入,用空格隔开。最后会生成一个cmakedemo可执行程序。当然,我们日常使用过程中,更多的是将源码编译成动/静态库。
目录结构如下:

目录结构

多源文件编译

# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)

# 工程名字 工程语言
project(cmakedemo C)

# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)

# 方法一:将多个文件加入到可执行程序中编译
# 将main.c MathFunctions.c文件加入到可执行程序cmakedemo中
#add_executable(cmakedemo2 main.c MathFunctions.c)

# 方法二:采用目录形式
# 将当前目录下的文件,都保存在DIR_SRCS变量中
aux_source_directory(. DIR_SRCS)
# 将变量代表的文件路径加入到可执行程序中编译
add_executable(cmakedemo2 ${DIR_SRCS})

CMakeLists.txt文件中,我们有两种方式将多个源码文件加入到编译,方法一将MathFunction.c文件放在add_executable最后,并用空格隔开。方法二中,我们用了一个aux_source_directory,第一个参数表示搜寻的目录,第二个参数DIR_SRCS表示将目录下的文件,表示成变量,并在下面应用。最后在add_executable中加入给变量${DIR_SRCS}
目录结构如下:

目录结构

多级目录

我们源码的目录结构,不会前两个例子中,都在同一级目录下,经常我们的源码有多级目录。这时候我们应该怎么编译呢?多级目录我们也有两种编译方式。

# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)

# 工程名字 工程语言
project(cmakedemo C)

# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)

## 方法一:
## 将当前目录下的文件,都保存在DIR_SRCS变量中
#aux_source_directory(. DIR_SRCS)
## 将math目录下的文件,都保存在DIR_MATH_SRCS变量中
#aux_source_directory(./math DIR_MATH_SRCS)
## 将变量代表的文件路径加入到可执行程序中编译
#add_executable(cmakedemo3 ${DIR_SRCS} ${DIR_MATH_SRCS})

# 方法二:
# 将当前目录下的文件,都保存在DIR_SRCS变量中
aux_source_directory(. DIR_SRCS)
# 将math目录加入编译
add_subdirectory(math)
# 将变量代表的文件路径加入到可执行程序中编译
add_executable(cmakedemo3 ${DIR_SRCS})
# 添加链接库
target_link_libraries(cmakedemo3 MathFunctions)

## 方法三:
## 将当前目录下的源码,都保存在DIR_SRCS变量中
#aux_source_directory(. DIR_SRCS)
## 将math目录下的文件,都保存在DIR_MATH_SRCS变量中
#aux_source_directory(./math DIR_MATH_SRCS)
## 将DIR_MATH_SRCS保存的文件,都编译进入静态库libMathFunctions.a
#add_library(MathFunctions ${DIR_MATH_SRCS})
## 将变量代表的文件路径加入到可执行程序中编译
#add_executable(cmakedemo3 ${DIR_SRCS})
## 添加链接库
#target_link_libraries(cmakedemo3 MathFunctions)

方法一将来自目录中的源文件,保存成DIR_MATH_SRCS变量,然后在add_executable中应用即可,这样就把多级目录下的源码都加入到构建中。方法二使用add_subdirectorymath子目录加入编译,这时候math中的CMakeLists.txt文件和源码也将作为一个编译子目录进行处理。target_link_libraries指定cmakedemo3可执行程序将链接MathFunctions库,MathFunctions库将在math子目录中生成。
math子目录CMakeLists.txt如下:

aux_source_directory(. DIR_MATH_SRCS)
add_library(MathFunctions ${DIR_MATH_SRCS})

add_library表示将默认生成libMathFunctions.a静态库。
目录结构如下:

目录结构

方法三是将子目录中的源码,都编译成libMathFuntions.a,最后同样的将静态库链接到目标可执行程序中,与方法二的区别是在通过一个CMakeLists.txt文件,就可以将静态库的编译包含在内,无需像方法二一样在./math目录下写一份CMakeLists.txt文件用于专门编译静态库。方法二和方法三各自有各自的好处,方法二更适合单模块编译,可以将某个目录下的源文件作为一个模块来编译,适合庞大的目录结构与模块层级编译。

自定义编译选项

# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)

# 工程名字 工程语言
project(cmakedemo C)

# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)

# 加入一个配置头文件,用于处理 CMake 对源码的设置
configure_file(
        "${PROJECT_SOURCE_DIR}/config.h.in"
        "${PROJECT_BINARY_DIR}/config.h"
)

# 设置USE_LOCALMATH打开
option(USE_LOCALMATH "TRUE USE LOCAL MATH LIBRARY" OFF)

if (USE_LOCALMATH)
    include_directories("${PROJECT_SOURCE_DIR}/math")
    add_subdirectory(math)

endif (USE_LOCALMATH)

aux_source_directory(. DIR_SRCS)

# 将变量代表的文件路径加入到可执行程序中编译
add_executable(cmakedemo4 ${DIR_SRCS})
# 添加链接库
if (USE_LOCALMATH)
    target_link_libraries(cmakedemo4 MathFunctions)
endif (USE_LOCALMATH)

在这里我们加入了一个config.h.in文件,这个文件,主要用来预定义宏,通过config.h.in,在编译之后可以生成config.h文件。config.h.in文件内容如下:

#cmakedefine USE_LOCALMATH

这里我们还使用到了option,主要是为了在进行编译时,在CMakeLists.txt同级目录下,通过ccmake .,来进行USE_LOCALMATH变量的选择,是否打开

开关变量

我们看到,最后有一个USE_LOCALMATH变量,可以用过enter键来选择ON或者OFF,如果是ON,那么在生成的config.h中,预定义宏被打开,如下:

#define USE_LOCALMATH

main.c中,我们就能够使用该宏定义了

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "config.h"
#ifdef USE_LOCALMATH
#include "math/MathFunctions.h"
#endif

int main(int argc, char *argv[]) {
    if (argc < 3) {
        printf("Usage: %s base exponent \n", argv[0]);
        return 1;
    }
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
#ifdef USE_LOCALMATH
    printf("Now we use our own Math library. \n");
    double result = power(base, exponent);
#else
    printf("Now we use the standard library. \n");
    double result = pow(base, exponent);
#endif

    printf("%g ^ %d is %g\n", base, exponent, result);
    return 0;
}

目录结构如下:


目录结构

环境检查

我们有时候需要在编译过程中,检查系统的环境,是否支持某些函数,这个例子中,我们检查是否编译环境自带pow函数,如果自带pow函数,则使用pow函数,如果没有则使用自定义的power函数。

# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)

# 工程名字 工程语言
project(cmakedemo C)

# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)

# 检查系统是否支持 pow 函数
include(${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
# 如果pow函数存在,则定义HAVE_POW宏,这个宏可以在下面的if条件中使用,也可以在config.h.in中预定义cmakedefine HAVE_POW
check_function_exists(pow HAVE_POW)

# 加入一个配置头文件,用于处理 CMake 对源码的设置
configure_file(
        "${PROJECT_SOURCE_DIR}/config.h.in"
        "${PROJECT_BINARY_DIR}/config.h"
)

# 如果宏未定义,则引入自定义的power函数
if (!HAVE_POW)
    include_directories("${PROJECT_SOURCE_DIR}/math")
    add_subdirectory(math)
endif (!HAVE_POW)

aux_source_directory(. DIR_SRCS)

# 将变量代表的文件路径加入到可执行程序中编译
add_executable(cmakedemo6 ${DIR_SRCS})
# 添加链接库
if (!HAVE_POW)
    target_link_libraries(cmakedemo4 MathFunctions)
endif (!HAVE_POW)

首先在顶层 CMakeLists.txt 文件中添加 CheckFunctionExists.cmake 宏,并调用 check_function_exists命令测试链接器是否能够在链接阶段找到 pow 函数。如果找到pow函数,则定义HAVE_POW宏,当然,在config.h.in中需要预定义HAVE_POW宏,如下:

#cmakedefine USE_LOCALMATH
#cmakedefine HAVE_POW
#cmakedefine HAVE_LOCALPOWER

随后在生成的config.h中,就会定义上该宏,如下:

/* #undef USE_LOCALMATH */
#define HAVE_POW
/* #undef HAVE_LOCALPOWER */

这时候,就可以在源码中使用宏了。

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "config.h"
#ifdef USE_LOCALMATH
#include "math/MathFunctions.h"
#endif

int main(int argc, char *argv[]) {
    if (argc < 3) {
        printf("Usage: %s base exponent \n", argv[0]);
        return 1;
    }
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
#ifndef HAVE_POW
    printf("Now we use our own Math library. \n");
    double result = power(base, exponent);
#else
    printf("Now we use the standard library. \n");
    double result = pow(base, exponent);
#endif

    printf("%g ^ %d is %g\n", base, exponent, result);

#ifdef HAVE_LOCALPOWER
    printf("HAVE_LOCALPOWER . \n");
#elif defined(HAVE_POW)
    printf("HAVE_POW . \n");
#endif
    return 0;
}

这里有个注意点check_function_exists需要在configure_file定义config.h.in之前调用,否则对于config.h.in中预定义的宏无效。同样的,在CMakeLists.txt中也能够使用HAVE_POW宏来判断是否引入math目录,是否将math作为子目录加入编译,最后是否链接MathFunctions静态库。

添加版本号

在应用程序中,维护库或者可执行程序的版本号是一个好的习惯,配合changelog,能够很直观的看到库的更新迭代过程。在cmake中我们怎样添加版本号管理呢?

# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)

# 工程名字 工程语言
project(cmakedemo C)

# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)

set(VERSION_MAJOR 1)
set(VERSION_MINOR 0)

config.h.in文件中,添加预定义

#define VERSION_MAJOR @VERSION_MAJOR@
#define VERSION_MINOR @VERSION_MINOR@

这样,在生成的config.h文件中就有VERSION_MAJORVERSION_MINOR的定义,在代码中使用如下:

printf("major version %d , minor version %d \n", VERSION_MAJOR, VERSION_MINOR);

编译动静态库

上面我们都是生成可执行程序,如果我们想要生成动态库或者静态库应该怎么做呢?

动态库

aux_source_directory(. DIR_MATH_SRCS)
add_library(MathFunctions SHARED ${DIR_MATH_SRCS})

生成动态库如下:


动态库

静态库

aux_source_directory(. DIR_MATH_SRCS)
add_library(MathFunctions STATIC ${DIR_MATH_SRCS})

生成静态库如下:

静态库

主要区别是在add_library时指定STATIC/SHARED参数即可。

基本操作总结

通过以上基本操作,我们了解了如何生成可执行程序,生成动/静态库,如何添加版本号、如何进行环境检查、如何预定义宏、如何对多级目录进行编译。对于cmake,我们已经有了一个大概的了解,后续继续讲一下在android中如何与cmake配合使用,来完成我们的目标。

CMake与Android

在android平台中,系统已经为我们内置了很多的原生api供我们链接调用,不同的系统api,android为我们提供了不同的库,具体可以参考Android NDK 原生 API。这些预构建的库,已经存在在android平台上了,我们无需将他们打包到apk中,因为NDK库已经是cmake搜索路径的一部分,所以找到提供库的名字,链接到所需库即可。那我们要怎么做才能使用这些库呢?

find_library用法

添加find_library()命令到你的cmake构建脚本用于定位ndk库路径,并且将路径存储变量中。你可以在脚本的其他地方使用这个变量,下面例子是查找android平台的log库,将路径存储在log-lib变量中。

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 )

接下来我们需要将ndk库,链接到我们的目标程序或者目标库中:

# 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} )

这里target_link_libraries含义是将${log-lib} 路径的ndk库,链接到native-lib.so中

添加预构建的动态库

添加一个预先构建的库,类似于为CMake指定另一个本地构建库。然而因为库已经预构建,你需要使用IMPORTED
告诉cmake,你需要引入库到你的构建工程中。

add_library( imported-lib
             SHARED
             IMPORTED )

这里只是指定了引入一个动态库,并且动态库名称存储在本地变量imported-lib中。接着需要设置该动态库imported-lib的属性,首先指定具体库的位置

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 )

这里set_target_properties()定义了一个imported-lib库的属性IMPORTED_LOCATION,指定了该库的在本地操作系统中的位置,这样,结合上面add_library我们就完整的在cmake中引入了一个动态库,并且存储在imported-lib本地变量中,待后续使用。
当然,我们引入了动态库还不够,编译时,经常还需要用到动态库的头文件,那么头文件该怎么引入呢?

include_directories(imported-lib/include/)

include_directories中是头文件在操作系统中的相对路径或者绝对路径,相对路径是对于当前CMakeLists.txt的位置而定。

添加预构建的静态库

添加预构建的静态库,与动态库类似,只是在add_libraryset_target_properties中有所不同

add_library(imported-static-lib STATIC IMPORTED)
set_target_properties(imported-static-lib PROPERTIES IMPORTED_LOCATION imported-lib/src/${ANDROID_ABI}/libimported-lib.a)

主要区别是静态库在add_library中是STATIC,而动态库是SHARED,静态库会编译进目标动态库中,而动态库,最后编译完apk后,通过APK Analyzer查看,在apk的lib/${ANDROID_ABI}/目录下,有你所链接的动态库。

编译过程构建静态库

在编译过程中,可能会存在整个c工程会很庞大,例如笔者目前工作中的一个工程源码就很庞大,有多个不同的模块,组件,多级目录。那这种情况下我们可以将某些组件,先编译成静态库,然后将静态库参与最终目标动态库的编译。参考CMake基本操作->多级目录章节,有三种方法可以参考。

多工程编译

多工程编译类似于CMake基本操作->多级目录章节中的方法二,这里就不重新讲。参考示例如下:

# 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 )

CMake与Android结合总结

本章主要讲解了cmake与android和结合,如何在android中使用cmake,cmake如何使用android平台自带的系统库,构建动/静态库的过程,以及多工程编译,这里已经基本满足我们日常NDK开发过程中遇到的大部分情况。

CMake与Gradle

未完待续......

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容