胡琪

为今天工作,为明天投资,为未来孵化一些东西!

【NDK开发之】使用cmake编译管理cpp文件

安卓NDK开发之java JNI技术中已经提到过对于安卓的NDK开发而言,本质上是2大知识体系组成,一部分是java的JNI技术。另一部分就是对c/c++源文件的编译和管理。比如使用ndk-build模式对mk文件进行编译或cmake模式对CMakeList文件进行编译。新版本的Android Studio默认使用cmake对JNI代码进行管理和编译。而不在推荐使用Android.mk和Application.mk进行编译选项配置。

AS向导生成的官方Demo的CMakeList示例分析

先来看下AS向导创建一个NDK工程之后为我们自动生成的CMakeList文件的内容:

该示例文件总共只涉及到四个cmake命令,虽然只有四个命令,但是这些命令足以构成一个完整的cmake编译脚本,其意义与作用如下所示

  • cmake_minimum_required(VERSION 3.4.1)是用来设置编译该脚本的cmake的最小版本,也就是说如果你设置的版本号大于你AS本地内置的cmake版本的话是无法编译的,不过一般该文件是AS向导自动创建,所以该版本会是ndk路径中的cmake的版本。因此基本不用过多关心。
  • add_library:从注释上可以看到作用是创建命名一个库,然后提供编译该库的源代码文件的相对路径供cmkae系统去编译该库(这里的相对路径是对该CMakeLIsts.txt文件而言的),比如示例中表示告知cmake构建系统使用相对路径src/main/cpp/native-lib.cpp的源代码文件编译一个名称叫做native-lib的共享库
  • find_library:从注释上可以看到作用是搜索指定的预构建库并将路径存储为某个变量值,比如示例中表示搜索NDK内置的log库,将其路径命名为log-lib
  • target_link_libraries:从注释上可以看到作用是指定需要链接到目标库(也就是前面使用add_libary创建的库)的其他库的名称,比如示例中表示将前面搜索到的名称为log-lib的库链接到native-lib库中。

通过上面的分析其实我们很容易知道事实上只需要一个cmake命令即可完成c/c++源文件编译为库文件的功能,即add_library命令,如果你不需要使用安卓的日志功能,那么后面的find_libary和target_link_libraries这2个命令是不需要的。

cmake常用命令介绍

前面对这些命令的分析都是基于谷歌官方demo的注释来分析的,而cmake命令和NDK开发也无直接关系,cmake是由cmke官方维护的,其官方文档地址为:https://cmake.org/cmake/help/v3.12/manual/cmake-commands.7.html只不过谷歌NDK开发中的cmke构建系统和官方cmake相比做了一些较小改动,比如增加了一些安卓系统的内置库(如安卓日志库),因此cmake官方命令在NDK中都是支持的。

add_lirary命令向cmake构建系统添加库文件

add_lirary命令格式如下:

其作用是使用命令中列出的源代码文件列表编译构建一个名为name的库,其各个参数的含义与作用如下:

  • name:必选参数,表示编译构建的库的名称
  • [STATIC | SHARED | MODULE]:必选参数,表示编译的目标库的类型。STATIC表示静态库,比如.a文件,SHARED表示共享库,也就是动态库,比如linux上的.so文件,win上的.dll文件。MODULE目前还不是很了解,一般很少用到。
  • EXCLUDE_FROM_ALL:可选参数,如果设置了该参数,表明该库在自动编译时会被排除,需要手动编译,一般测试代码的编译可设置该参数,这样发布版本时候就无需修改cmake文件而达到不编译测试代码的效果,非测试代码无需设置该参数。该参数在大型的c/c++项目编译中可能会用到,一般的NDK项目基本无需用到该参数。
  • [source1] [source2 …]:必须参数。需要编译的源代码文件路径,多个路径之间用空格隔开,如果是相对路径,则该相对路径是相对该CMakeLists文件而言的。

target_link_libraries命令向目标库文件链接其他库文件

target_link_libraries命令包含多个签名格式,其通用格式如下:

其作用是将名为item(可以为多个,如果为多个,以空格隔开)的库链接到目标库target上,比如在官方示例demo中,native-lib这个目标库使用到了安卓日志库,所以需要将log-lib链接到native-lib上。如果你的代码中使用到了第三方的库或系统内置库,需要使用该命令将其链接到目标库中。

上述命令即表示将三方库jsoncpp和安卓系统内置日志库链接到native-lib目标库中。这里就涉及到另外一个知识点引入其他本地已经存在的库。

注意:使用该命令需要确保前面已经使用add_executable()或者add_lirary创建了名为target的库。

使用add_lirary的IMPORTED模式配合set_target_properties命令引入已经存在的本地库

add_lirary命令还可以用来引入一个本地已经存在的库,此时其命令签名如下

  • name:必选参数,表示要引入的本地已经存在的库的命令,该名称名称在其创建所在的目录中有作用域,可以被其他命令所引用
  • <SHARED|STATIC|MODULE|OBJECT|UNKNOWN>:必选参数,如果要引入的本地库是.dll或者.so文件,那么该选项为SHARED,.a文件为STATIC,.o文件为OBJECT,MODULE和UNKNOWN一般较少用到
  • IMPORTED:必选参数,表明该add_libary的作用是引入一个本地已经存在的库文件
  • GLOBAL:可选参数,前面说过该命令创建的名为name的库名称的作用域为创建该文件的目录,该选项可扩展该可见性(The target name has scope in the directory in which it is created and below, but the GLOBAL option extends visibility)。该选项通常用在如果有多个目录下包含CMakeLists.txt文件的时候控制该name的作用域。

聪明的同学可能已经想到了该命令怎么没指定本地库的路径呢?这就需要配合另外一个命令set_target_properties了,该命令签名如下:

  • target1 target2 …:要设置属性值的目标库名称,可以为多个,多个以空格隔开
  • PROPERTIES prop1 value1
    prop2 value2 …  :要设置的属性的属性名称和其对应的值,多个属性名和值之间用空格隔开。属性名称需要是cmake内置的系统变量名称,比如表示引入库路径的属性:IMPORTED_LOCATION,表示安卓API版本的属性:ANDROID_API,其他更多属性可以从官网查询https://cmake.org/cmake/help/v3.12/manual/cmake-properties.7.html#id4

所以如果我们要引入jsoncpp这个三方库,只需要如下命令即可:

注意对于引入本地已经存在的库而言,在链接之间需要编译通过,因此需要先使用include_directories命令将本地库的头文件包含进来。

使用include_directories包含需要引入的头文件的路径

正如前面示例中提到的,如果我们要使用本地已经构建好的静态库.a或者动态库.so,则在代码中需要使用这些库中用到的头文件,在编译时需要将这些头文件的路径添加到构建脚本中

  • AFTER|BEFORE:可选参数,因为include_directories命令本质上是将一些路径添加到cmake编译时头文件查找路径列表中,类似win上添加系统环境变量,所以包括添加到最前面和在最末尾追加这两种方式。AFTER追加到末尾,BEFORE前置添加。一般无需设置。
  • SYSTEM:可选参数,一般无用设置
  • dir1 [dir2 …]:必选参数,要引入的头文件的路径,可以为多个,但至少要有1个

使用file命令匹配通配符的文件列表

聪明的同学可能意识到这一点了,前面的add_lirary命令在指定需要编译的c/cpp文件路径时是一个一个指定的,多个之间用空格隔开,如果项目中c/cpp文件比较多,那么这个命令写起来很麻烦,而且如果我们的CMakeLists脚本已经写好了,在下次增加新c/cpp文件的时候需要将这些新增加的c/cpp文件路径给添加进去,这么做很显然不符合一个优秀工程师所期望的高效快速的开发流程。我们通常希望能够使用通配符自动将某个路径下的所有符合通配符规则的c/cpp文件添加到编译文件列表中。file命令就能够实现这个功能。

file命令和通配符相关的命令作用是生成与通配符匹配的文件列表,并将其存储到variable变量中,该命令包含2种基本形式,一种是GLOB,一种是GLOB_RECURSE,前者表示只对给出的当前目录下符合通配符规则的文件进行匹配,后者表示对当前目录及其子目录下的符合通配符规则的文件进行匹配,也就是递归匹配。其他各参数意义及其作用如下所示:

  • variable:必选参数,表示将file命令匹配到的文件列表存储到哪个变量中
  • FOLLOW_SYMLINKS:GLOB_RECURSE模式的可选参数,如果该参数被设置,LIST_DIRECTORIES 将符号链接作为路径(暂不是很明白官网这段描述)。一般不用设置
  • LIST_DIRECTORIES:可选参数,默认情况下,匹配结果只会包含文件,而不会包含目录,如果将该选项设置为true,则会将目录添加到匹配结果列表中,一般不用设置。
  • RELATIVE:可选参数,如果设置了该参数,则返回结果列表会返回相对与该参数设置的路径的相对路径,一般不用设置
  • CONFIGURE_DEPENDS:可选参数,如果设置该参数,cmake将在编译时给主构建系统添加逻辑来检查目标,以重新运 GLOB 标志的命令(暂时不是很理解官网上的这段描述),一般不用设置
  • globbing-expressions:必选参数,通配符表达式

上面的最后一个参数通配符表达式一般的通配符表达式都支持,比如*,?,[1-5]等,如下是官网的一个小示例:

所以如果我们要把ndk工程下的src/main/cpp目录下的所有目录及其子目录下的cpp文件都匹配到,那么可以通过如下命令完成:

其中GLOB_RECURSE表示递归匹配, all_source_files 是设置的变量名,”${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/*.cpp”)是通配符表达式,表示将src/main/cpp目录下的所有cpp文件都添加到all_source_files这个变量列表中,CMAKE_CURRENT_SOURCE_DIR是cmake内置的一个系统变量,表示该CMakeLists文件的路径。

所以前面的add_libary命令就可以改写为如下形式:

这样即使后面随着项目功能的扩展,在cpp目录下新增加了其他的cpp文件,会自动通过通配符添加到源文件编译列表中,就不用手动修改脚本了。

Tips:对于cmake官方文档上的命令介绍而言,可选参数都是通过[]包裹,必选参数是通过<>包裹。

一个完整的CMakeLists示例

在gradle文件中配置cmake选项

前面介绍了NDK开发中CMakeLists脚本的编写,其实对于一些通用的配置项,可以直接在gradle中进行配置,比如最常见的编译选项CFlags和CXXFlags,所以推荐大家对于常规的通用的编译选项直接在gradle的cmake节点中进行配置,而对于不通用的,需要细分的编译选项在CMakeLists脚本中进行配置比如后面会讲到对so体积进行优化时一个很重要的点就是编译参数的选择,对于x86架构的so和arm架构的so而言体积最优的编译选项是不一样的,比如实践发现对于gcc编译而言,x86架构使用-O2参数往往编译体积最小,而对于arm而言-OS参数编译体积最小。那么这些需要通过判断ANDROID_ABI类型来设置不同编译选项的工作就适合放到CMakeLists脚本中,而对于其他的每个abi类型都适合的参数可以直接在gradle中进行设置,这样能够最大化的简化CMakeLists编译构建脚本。所以掌握在gradle文件中配置cmake编译选项是非常重要的。

在gradle文件中配置cmake编译选项和传统的ndk-build类似,主要包括2个步骤:

  1. build.gradle文件的android节点下添加externalNativeBuild{}块,然后在该块下增加cmake{}块,在该节点下设置cmake构建脚本的路径。
  2. build.gradle文件的android节点下的defaultConfig{}块中配置一个externalNativeBuild{}块,在该节点下指定cmake块的编译参数

举个例子:

设置cmake构建脚本路径

配置cmake编译参数

新版本的AS,在创建一个NDK工程的时候默认使用cmake模式,此时会自动生成上述配置,因此我们只需要针对自己具体的项目情况配置符合自己项目的cmake编译选项即可,推荐大家将一些通用的编译选项直接在gradle中进行配置,像上面的示例那样,而需要针对不同情况(如不同abi类型)具体配置的编译选项放在CMakeLists文件中进行设置,这样能大大简化CMakeLists构建脚本。

打赏

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注