Android Studio 从 2.2 版本起开始支持 CMake ,能够经过 CMake 和 NDK 将 C/C++ 代码编译成底层的库,而后再配合 Gradle 的编译将库打包到 APK 中。html
这意味就不须要再编写 .mk
文件来编译 so
动态库了。android
CMake 是一个跨平台构建系统,在 Android Studio 引入 CMake 以前,它就已经被普遍运用了。ios
Google 官方网站上有对 CMake 的使用示范,能够参考 官方指南。git
总结官网对 CMake 的使用,其实也就以下的步骤:github
.c
或 .cpp
文件包含指定。若是你对上面的步骤仍是不了解,那么接下来就更深刻了解 CMake 相关内容吧~~~express
以 Clion 做为工具来说解 CMake 的基本使用。bash
一个打印 hello world 的 cpp 文件,经过 CMake 将它编译成可执行文件。微信
在 cpp 的同一目录下建立 CMakeLists.txt
文件,内容以下:markdown
# 指定 CMake 使用版本 cmake_minimum_required(VERSION 3.9) # 工程名 project(HelloCMake) # 编译可执行文件 add_executable(HelloCMake main.cpp ) 复制代码
其中,经过 cmake_minimum_required
方法指定 CMake 使用版本,经过 project
指定工程名。ide
而 add_executable
就是指定最后编译的可执行文件名称和须要编译的 cpp 文件,若是工程很大,有多个 cpp 文件,那么都要把它们添加进来。
定义了 CMake 文件以后,就能够开始编译构建了。
CMake 在构建工程时会生成许多临时文件,避免让这些临时文件污染代码,通常会把它们放到一个单独的目录中。
操做步骤以下:
# 在 cpp 目录下建立 build 目录 mkdir build # 调用 cmake 命令生成 makefile 文件 cmake .. # 编译 make 复制代码
在 build 目录中能够找到最终生成的可执行文件。
这就是 CMake 的一个简单操做,将 cpp 编译成可执行文件,但在 Android 中,大多数场景都是把 cpp 编译成库文件。
一样仍是一个 cpp 文件和一个 CMake 文件,cpp 文件内容为打印字符串的函数:
#include <iostream> void print() { std::cout << "hello lib" << std::endl; } 复制代码
同时,CMake 文件也要作相应更改:
cmake_minimum_required(VERSION 3.12) # 指定编译的库和文件,SHARED 编译动态库 add_library(share_lib SHARED lib.cpp) # STATIC 编译静态库 # add_library(share_lib STATIC lib.cpp) 复制代码
经过 add_library
指定要编译的库的名称,以及动态库仍是静态库,还有要编译的文件。
最后一样地执行构建,在 build 目录下能够看到生成的库文件。
到这里,就基本可使用 CMake 来构建 C/C++ 工程了。
熟悉了上面的基本操做以后,就必然会遇到如下的问题了:
带着这些问题,仍是要继续深刻学习 CMake 的相关语法,最好的学习材料就是 官网文档 了。
为了不直接看官方文档时一头雾水,这里列举一些经常使用的语法命令。
在前面就已经用到了 CMake 注释了,每一行的开头 #
表明注释。
另外,CMake 的全部语法指令是不区分大小写的。
经过 set
来定义变量:
# 变量名为 var,值为 hello set(var hello) 复制代码
当须要引用变量时,在变量名外面加上 ${}
符合来引用变量。
# 引用 var 变量 ${var} 复制代码
还能够经过 message
在命令行中输出打印内容。
set(var hello) message(${var}) 复制代码
CMake 中经过 math
来实现数学操做。
# math 使用,EXPR 为大小 math(EXPR <output-variable> <math-expression>) 复制代码
math(EXPR var "1+1") # 输出结果为 2 message(${var}) 复制代码
math 支持 +, -, *, /, %, |, &, ^, ~, <<, >>
等操做,和 C 语言中大体相同。
CMake 经过 string
来实现字符串的操做,这波操做有不少,包括将字符串所有大写、所有小写、求字符串长度、查找与替换等操做。
具体查看 官方文档。
set(var "this is string") set(sub "this") set(sub1 "that") # 字符串的查找,结果保存在 result 变量中 string(FIND ${var} ${sub1} result ) # 找到了输出 0 ,不然为 -1 message(${result}) # 将字符串所有大写 string(TOUPPER ${var} result) message(${result}) # 求字符串的长度 string(LENGTH ${var} num) message(${num}) 复制代码
另外,经过空白或者分隔符号能够表示字符串序列。
set(foo this is a list) // 实际内容为字符串序列 message(${foo}) 复制代码
当字符串中须要用到空白或者分隔符时,再用双括号""
表示为同一个字符串内容。
set(foo "this is a list") // 实际内容为一个字符串 message(${foo}) 复制代码
CMake 中经过 file
来实现文件操做,包括文件读写、下载文件、文件重命名等。
具体查看 官方文档
# 文件重命名 file(RENAME "test.txt" "new.txt") # 文件下载 # 把文件 URL 设定为变量 set(var "http://img.zcool.cn/community/0117e2571b8b246ac72538120dd8a4.jpg") # 使用 DOWNLOAD 下载 file(DOWNLOAD ${var} "/Users/glumes/CLionProjects/HelloCMake/image.jpg") 复制代码
在文件的操做中,还有两个很重要的指令 GLOB
和 GLOB_RECURSE
。
# GLOB 的使用 file(GLOB ROOT_SOURCE *.cpp) # GLOB_RECURSE 的使用 file(GLOB_RECURSE CORE_SOURCE ./detail/*.cpp) 复制代码
其中,GLOB
指令会将全部匹配 *.cpp
表达式的文件组成一个列表,并保存在 ROOT_SOURCE
变量中。
而 GLOB_RECURSE
指令和 GLOB
相似,可是它会遍历匹配目录的全部文件以及子目录下面的文件。
使用 GLOB
和 GLOB_RECURSE
有好处,就是当添加须要编译的文件时,不用再一个一个手动添加了,同一目录下的内容都被包含在对应变量中了,但也有弊端,就是新建了文件,可是 CMake 并无改变,致使在编译时也会从新产生构建文件,要解决这个问题,就是动一动 CMake,让编译器检测到它有改变就行了。
在 CMake 中有许多预约义的常量,使用好这些常量能起到事半功倍的效果。
好比,在 add_library
中须要指定 cpp 文件的路径,以 CMAKE_CURRENT_SOURCE_DIR
为基准,指定 cpp 相对它的路径就行了。
# 利用预约义的常量来指定文件路径 add_library( # Sets the name of the library. openglutil # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). ${CMAKE_CURRENT_SOURCE_DIR}/opengl_util.cpp ) 复制代码
CMake 可以用来在 Window、Linux、Mac 平台下进行编译,在它的内部也定义了和这些平台相关的变量。
具体查看 官方文档 。
列举一些常见的:
有了这些常量作区分,就能够在一份 CMake 文件中编写不一样平台的编译选项。
if(WIN32){ # do something }elseif(UNIX){ # do something } 复制代码
具体参考cmake-commands ,这里面包括了不少重要且常见的指令。
简单示例 CMake 中的函数操做:
function(add a b) message("this is function call") math(EXPR num "${a} + ${b}" ) message("result is ${aa}") endfunction() add(1 2) 复制代码
其中,function 为定义函数,第一个参数为函数名称,后面为函数参数。
在调用函数时,参数之间用空格隔开,不要用逗号。
宏的使用与函数使用有点相似:
macro(del a b) message("this is macro call") math(EXPR num "${a} - ${b}") message("num is ${num}") endmacro() del(1 2) 复制代码
在流程控制方面,CMake 也提供了 if、else 这样的操做:
set(num 0) if (1 AND ${num}) message("and operation") elseif (1 OR ${num}) message("or operation") else () message("not reach") endif () 复制代码
其中,CMake 提供了 AND
、OR
、NOT
、LESS
、EQUAL
等等这样的操做来对数据进行判断,好比 AND
就是要求两边同为 True 才行。
另外 CMake 还提供了循环迭代的操做:
set(stringList this is string list) foreach (str ${stringList}) message("str is ${str}") endforeach () 复制代码
CMake 还提供了一个 option
指令。
能够经过它来给 CMake 定义一些全局选项:
option(ENABLE_SHARED "Build shared libraries" TRUE) if(ENABLE_SHARED) # do something else() # do something endif() 复制代码
可能会以为 option
无非就是一个 True or False 的标志位,能够用变量来代替,但使用变量的话,还得添加 ${}
来表示变量,而使用 option
直接引用名称就行了。
明白了上述的 CMake 语法以及从官网去查找陌生的指令意思,就基本上能够看懂大部分的 CMake 文件了。
这里举两个开源库的例子:
glm
是一个用来实现矩阵计算的,在 OpenGL 的开发中会用到。CMakeLists.txt
地址在 这里libjpeg-turbo
是用来进行图片压缩的,在 Android 底层就是用的它。CMakeLists.txt
地址在 这里这两个例子中大量用到了前面所讲的内容,能够试着读一读增长熟练度。
接下来再回到用 CMake 编译动态库的话题上,毕竟 Android NDK 开发也主要是用来编译库了,当编译完 so 以后,咱们能够对它作一些操做。
经过 set_target_properties
来给编译的库设定相关属性内容,函数原型以下:
set_target_properties(target1 target2 ... PROPERTIES prop1 value1 prop2 value2 ...) 复制代码
好比,要将编译的库改个名称:
set_target_properties(native-lib PROPERTIES OUTPUT_NAME "testlib" ) 复制代码
更多的属性内容能够参考 官方文档。
不过,这里面有一些属性设定无效,在 Android Studio 上试了无效,在 CLion 上反而能够,固然也多是我使用姿式不对。
好比,实现动态库的版本号:
set_target_properties(native-lib PROPERTIES VERSION 1.2 SOVERSION 1 ) 复制代码
对于已经编译好的动态库,想要把它导入进来,也须要用到一个属性。
好比编译的 FFmpeg
动态库,
# 使用 IMPORTED 表示导入库
add_library(avcodec-57_lib SHARED IMPORTED)
# 使用 IMPORTED_LOCATION 属性指定库的路径
set_target_properties(avcodec-57_lib PROPERTIES IMPORTED_LOCATION
${CMAKE_CURRENT_SOURCE_DIR}/src/main/jniLibs/armeabi/libavcodec-57.so )
复制代码
若是编译了多个库,而且想库与库之间进行连接,那么就要经过 target_link_libraries
。
target_link_libraries( native-lib glm turbojpeg log ) 复制代码
在 Android 底层也提供了一些 so 库供上层连接使用,也要经过上面的方式来连接,好比最多见的就是 log
库打印日志。
若是要连接本身编译的多个库文件,首先要保证每一个库的代码都对应一个 CMakeLists.txt
文件,这个 CMakeLists.txt
文件指定当前要编译的库的信息。
而后在当前库的 CMakeLists.txt
文件中经过 ADD_SUBDIRECTORY
将其余库的目录添加进来,这样才可以连接到。
ADD_SUBDIRECTORY(src/main/cpp/turbojpeg) ADD_SUBDIRECTORY(src/main/cpp/glm) 复制代码
在使用的时候有一个容易忽略的步骤就是添加头文件,经过 include_directories
指令把头文件目录包含进来。
这样就能够直接使用 #include "header.h"
的方式包含头文件,而不用 #include "path/path/header.h"
这样添加路径的方式来包含。
以上,就是关于 CMake 的部分总结内容。
欢迎关注微信公众号:【纸上浅谈】,得到最新文章推送~~~