欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android NDK 开发:CMake 使用方法

程序员文章站 2022-05-01 17:43:44
android ndk 开发:cmake 使用方法。当在做 android ndk 开发时,如果不熟悉用 cmake 来构建,读不懂 cmakelists.txt 的配置脚本,很容易就会踩坑,遇到编...

android ndk 开发:cmake 使用方法。当在做 android ndk 开发时,如果不熟悉用 cmake 来构建,读不懂 cmakelists.txt 的配置脚本,很容易就会踩坑,遇到编译失败,一个很小的配置问题都会浪费很多时间。所谓工欲善其事必先利其器,学习 ndk 开发还是要大致了解 cmake 的基本语法和配置的。下面文章是根据 cmake 实践手册 做的一些简短笔记,具体说得不够详细的地方,可以查看手册。

2. cmake 是什么?

cmake 是一个开源的跨平台自动化构建。官网地址:cmake

2.1cmake 的特点

1)开放源代码,使用类 bsd 许可发布。2)跨平台,并可生成 native 编译配置文件,在 linux/unix 平台,生成 makefile,在
mac 平台,可以生成 xcode,在 windows 平台,可以生成 msvc 的工程文件。3)能够管理大型项目;4)简化编译构建过程和编译过程。cmake 的工具链非常简单:cmake+make。5)高效率;6)可扩展,可以为 cmake 编写特定功能的模块,扩充 cmake 功能。

2.2 使用建议

1)如果你没有实际的项目需求,那么看到这里就可以停下来了,因为 cmake 的学习过程就是实践过程,没有实践,读的再多几天后也会忘记;
2)如果你的工程只有几个文件,直接编写 makefile 是最好的选择;(那得学习 make 命令和熟悉 makefile 的构建规则,这是另外一回事了)
3)如果使用的是 c/c++/java 之外的语言,请不要使用 cmake;
4)如果你使用的语言有非常完备的构建体系,比如 java 的 ant,也不需要学习 cmake;
5)如果项目已经采用了非常完备的工程管理工具,并且不存在维护问题,没有必要迁移到cmake

cmakelists.txt 文件是 cmake 的构建定义文件。如果工程存在多个目录,需要在每个要管理的目录都添加一个 cmakelists.txt 文件。

3. cmake 命令

cmake 命令行格式有很多种,这里只介绍一种比较常用的

cmake [] ( | )

options 为可选项,为空时,构建的路径为当前路径。
options 的值,可以通过输入cmake --help 或到官方文档cmake-cmake查看,比如:
-g 是指定构建系统的生成器,当前平台所支持的 generator-name 也可以通过cmake --help查看。(options 一般默认为空就好,这里不做过多介绍)

path-to-source和path-to-existing-build二选一,分别表示 cmakelists.txt 所在的路径和一个已存在的构建工程的目录

cmake .表示构建当前目录下 cmakelists.txt 的配置,并在当前目录下生成 makefile 等文件;【属于内部构建】cmake ..表示构建上一级目录下 cmakelists.txt 的配置,并在当前目录下生成 makefile 等文件;cmake [参数] [指定进行编译的目录或存放makefile文件的目录] [指定cmakelists.txt文件所在的目录] 【属于外部构建】

附:内部构建(in-source build)与外部构建(out-of-source build)
内部构建生成的临时文件可能比源代码还要多,非常影响工程的目录结构和可读性。而cmake 官方建议使用外部构建,外部构建可以达到将生成中间产物与源代码分离。

4. hello world cmake

注:以下 mac 平台

安装 cmake (windows 可以到官网下载安装包安装 download | cmake

brew install cmakebrew link cmakecmake -version #检验是否安装成功,显示对应 cmake 版本号即表示安装成功

创建一个 cmake/t1 目录,并分别编写 main.c 和 cmakelists.txt (cmakelists.txt 是 cmake 的构建定义文件)

#include int main(){ printf(“hello world from cmake!\n”); return 0;}

project(hello)set(src_list main.c)message(status "this is binary dir " ${hello_binary_dir}) #终端打印的信息message(status "this is source dir "${hello_source_dir})add_executable(hello ${src_list})

这里如果直接输入cmake .开始构建,属于内部构建。建议采用外部构建的方法,先建一个 build 文件夹,进入 build 文件夹在执行cmake ..。构建后出现很多 log 包含以下,说明构建成功,并且目录下会生成cmakefiles, cmakecache.txt, cmake_install.cmake, makefile 等文件

-- this is binary dir /users/cfanr/androidstudioprojects/ndk/cmake/t1-- this is source dir /users/cfanr/androidstudioprojects/ndk/cmake/t1-- configuring done-- generating done-- build files have been written to: /users/cfanr/androidstudioprojects/ndk/cmake/t1

然后在执行 make命令,会生成 main.c 对应的可执行文件hello,并会出现以下彩色的 log

[ 50%] building c object cmakefiles/hello.dir/main.c.o[100%] linking c executable hello[100%] built target hello

最后执行 ./hello 会打印输出:
hello world from cmake!

5. cmake 的基本语法规则

使用星号 # 作为注释;变量使用 ${} 方式取值,但是在 if 控制语句中是直接使用变量名;指令名(参数1 参数2 …),其中参数之间使用空格或分号隔开;指令与大小写无关,但参数和变量是大小写相关的;

6. cmake 的常用指令

注:指令与大小写无关,官方建议使用大写,不过 android 的 cmake 指令是小写的,下面为了便于,采取小写的方式。

6.1 project 指令

语法:project( [cxx] [c] [java])
这个指令是定义工程名称的,并且可以指定工程支持的语言(当然也可以忽略,默认情况表示支持所有语言),不是强制定义的。例如:project(hello)
定义完这个指令会隐式定义了两个变量:
_binary_dir和_source_dir
由上面的例子也可以看到,message 指令有用到这两个变量;

另外 cmake 系统还会预定义了 project_binary_dir 和 project_source_dir 变量,它们的值和上面两个的变量对应的值是一致的。不过为了统一起见,建议直接使用project_binary_dir 和project_source_dir,即使以后修改了工程名字,也不会影响两个变量的使用。

6.2 set 指令

语法:set(var [value])
这个指令是用来显式地定义变量,多个变量用空格或分号隔开
例如:set(src_list main.c test.c)

注意,当需要用到定义的 src_list 变量时,需要用${var}的形式来引用,如:${src_list}
不过,在 if 控制语句中可以直接使用变量名。

6.3 message 指令

语法:message([send_error | status | fatal_error] “message to display” … )
这个指令用于向终端输出用户定义的信息,包含了三种类型:
send_error,产生错误,生成过程被跳过;
status,输出前缀为—-的信息;(由上面例子也可以看到会在终端输出相关信息)
fatal_error,立即终止所有 cmake 过程;

6.4 add_executable 指令

语法:add_executable(executable_file_name [source])
将一组源文件 source 生成一个可执行文件。 source 可以是多个源文件,也可以是对应定义的变量
如:add_executable(hello main.c)

6.5 cmake_minimun_required(version 3.4.1)

用来指定 cmake 最低版本为3.4.1,如果没指定,执行 cmake 命令时可能会出错

6.6 add_subdirectory 指令

语法:add_subdirectory(source_dir [binary_dir] [exclude_from_all])
这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置。exclude_from_all参数含义是将这个目录从编译过程中排除。

另外,也可以通过 set 指令重新定义 executable_output_path 和 library_output_path 变量来指定最终的目标二进制的位置(指最终生成的 hello 或者最终的共享库,不包含编译生成的中间文件)
set(executable_output_path ${project_binary_dir}/bin)
set(library_output_path ${project_binary_dir}/lib)

6.7 add_library 指令

语法:add_library(libname [shared | static | module] [exclude_from_all] [source])
将一组源文件 source 编译出一个库文件,并保存为 libname.so (lib 前缀是生成文件时 cmake自动添加上去的)。其中有三种库文件类型,不写的话,默认为 static:

shared: 表示动态库,可以在(java)代码中使用 system.loadlibrary(name) 动态调用;static: 表示静态库,集成到代码中会在编译时调用;module: 只有在使用 dyid 的系统有效,如果不支持 dyid,则被当作 shared 对待;exclude_from_all: 表示这个库不被默认构建,除非其他依赖或手工构建

#将compress.c 编译成 libcompress.so 的共享库add_library(compress shared compress.c)

add_library 命令也可以用来导入第三方的库:
add_library(libname [shared | static | module | unknown] imported)
如,导入 libjpeg.so

add_library(libjpeg shared imported)

导入库后,当需要使用 target_link_libraries 链接库时,可以直接使用该库

6.8 find_library 指令

语法:find_library( name1 path1 path2 …)
var 变量表示找到的库全路径,包含库文件名 。例如:

find_library(libx x11 /usr/lib)find_library(log-lib log) #路径为空,应该是查找系统环境变量路径

6.9 set_target_properties 指令

语法: set_target_properties(target1 target2 … properties prop1 value1 prop2 value2 …)
这条指令可以用来设置输出的名称(设置构建同名的动态库和静态库,或者指定要导入的库文件的路径),对于动态库,还可以用来指定动态库版本和 api 版本。
如,set_target_properties(hello_static properties output_name “hello”)
设置同名的 hello 动态库和静态库:

set_target_properties(hello properties clean_direct_output 1)set_target_properties(hello_static properties clean_direct_output 1)

指定要导入的库文件的路径

add_library(jpeg shared imported)#注意要先 add_library,再 set_target_propertiesset_target_properties(jpeg properties imported_location ${project_source_dir}/libs/${android_abi}/libjpeg.so)

设置动态库 hello 版本和 api 版本:
set_target_properties(hello properties version 1.2 soversion 1)

和它对应的指令:
get_target_property(var target property)
如上面的例子,获取输出的库的名字

get_target_property(output_value hello_static output_name)message(status "this is the hello_static output_name:"${output_value})

6.10 include_directories 指令

语法:include_directories([after | before] [system] dir1 dir2…)
这个指令可以用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来,默认的行为是追加到当前的头文件搜索路径的
后面。

6.11 target_link_libraries 指令

语法:target_link_libraries(target library library2…)
这个指令可以用来为 target 添加需要的链接的共享库,同样也可以用于为自己编写的共享库添加共享库链接。
如:

#指定 compress 工程需要用到 libjpeg 库和 log 库target_link_libraries(compress libjpeg ${log-lib})

同样,link_directories(directory1 directory2 …) 可以添加非标准的共享库搜索路径。

还有其他 file、list、install 、find_ 指令和控制指令等就不介绍了,详细可以查看手册。

7. cmake 的常用变量

7.1 变量引用方式

使用 ${} 进行变量的引用。不过在 if 等语句中,可以直接使用变量名而不用通过 ${} 取值

7.2 自定义变量的方式

主要有隐式定义和显式定义两种。隐式定义,如 project 指令会隐式定义_binary_dir 和 _source_dir
而对于显式定义就是通过 set 指令来定义。如:set(hello_src main.c)

7.3 cmake 常用变量

1)cmake_binary_dir, project_binary_dir, _binary_dir
这三个变量指代的内容都是一样的,如果是 in-source 编译,指的是工程顶层目录,如果是 out-of-source 编译,指的是工程编译发生的目录。

2)cmake_source_dir, project_source_dir, _source_dir
这三个变量指代的内容也是一样的,不论哪种编译方式,都是工程顶层目录。

3)cmake_current_source_dir
当前处理的 cmakelists.txt 所在的路径

4)cmake_current_binary_dir
如果是 in-source 编译,它跟 cmake_current_source_dir 一致,如果是 out-of-source 编译,指的是 target 编译目录。
使用 add_subdirectory(src bin)可以修改这个变量的值;
而使用 set(executable_output_path < 新路径>) 并不会对这个变量造成影响,它仅仅修改了最终目标文件存放的路径。

5)cmake_current_list_file
输出调用这个变量的 cmakelists.txt 的完整路径

6)cmake_current_list_line
输出这个变量所在的行

7)cmake_module_path
这个变量用来定义自己的 cmake 模块所在的路径。如果你的工程比较复杂,有可能会自己
编写一些 cmake 模块,这些 cmake 模块是随你的工程发布的,为了让 cmake 在处理
cmakelists.txt 时找到这些模块,你需要通过 set 指令,将自己的 cmake 模块路径设
置一下。
比如 set(cmake_module_path ${project_source_dir}/cmake)
这时候你就可以通过 include 指令来调用自己的模块了。

8)executable_output_path 和 library_output_path
分别用来重新定义最终结果的存放目录,前面我们已经提到了这两个变量。

9)project_name
返回通过 project 指令定义的项目名称。

8. android cmake 的使用

8.1 cmakelist.txt 的编写

再回归到 android ndk 开发中 cmake 的使用,先看一个系统生成的 ndk 项目的 cmakelists.txt 的配置:( 去掉原有的注释)

#设置编译 native library 需要最小的 cmake 版本cmake_minimum_required(version 3.4.1)#将指定的源文件编译为名为 libnative-lib.so 的动态库add_library(native-lib shared src/main/cpp/native-lib.cpp)#查找本地 log 库find_library(log-lib log)#将预构建的库添加到自己的原生库target_link_libraries(native-lib ${log-lib} )

复杂一点的 cmakelists,这是一个本地使用 libjpeg.so 来做图片压缩的项目

cmake_minimum_required(version 3.4.1)#设置生成的so动态库最后输出的路径set(cmake_library_output_directory ${project_source_dir}/src/main/jnilibs/${android_abi})#指定要引用的libjpeg.so的头文件目录set(libjpeg_include_dir src/main/cpp/include)include_directories(${libjpeg_include_dir})#导入libjpeg动态库 shared;静态库为staticadd_library(jpeg shared imported)#对应so目录,注意要先 add_library,再 set_target_properties)set_target_properties(jpeg properties imported_location ${project_source_dir}/libs/${android_abi}/libjpeg.so)add_library(compress shared src/main/cpp/compress.c)find_library(graphics jnigraphics)find_library(log-lib log)#添加链接上面个所 find 和 add 的 librarytarget_link_libraries(compress jpeg ${log-lib} ${graphics})

8.2 配置 gradle

简单的配置如下,至于 cppflags 或 cflags 的参数有点复杂,一般设置为空或不设置也是可以的,这里就不过多介绍了

android { compilesdkversion 25 buildtoolsversion "25.0.3" defaultconfig { minsdkversion 15 targetsdkversion 25 versioncode 1 versionname "1.0" externalnativebuild { cmake { // passes optional arguments to cmake. arguments "-dandroid_arm_neon=true", "-dandroid_toolchain=clang" // sets optional flags for the c compiler. cflags "-d_example_c_flag1", "-d_example_c_flag2" // sets a flag to enable format macro constants for the c++ compiler. cppflags "-d__stdc_format_macros" //生成.so库的目标平台 abifilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'arm64-v8a' } } } //配置 cmakelists.txt 路径 externalnativebuild { cmake { path "cmakelists.txt" } }}

对于 cmake 的知识点其实还是有很多的,这里只是简单介绍了 cmake 的基本语法规则和使用方法,了解了这些,遇到问题应该也能快速定位到原因,找出解决的版本,就算不记得一些指令,也通过查找文档解决。能达到这种程度,对于 android ndk 开发来说,掌握这些也足够了吧。