Android NDK 開(kāi)發(fā)之 CMake 必知必會(huì)
Android Studio 從 2.2 版本起開(kāi)始支持 CMake ,可以通過(guò) CMake 和 NDK 將 C/C++ 代碼編譯成底層的庫(kù),然后再配合 Gradle 的編譯將庫(kù)打包到 APK 中。
這意味就不需要再編寫(xiě) .mk 文件來(lái)編譯 so 動(dòng)態(tài)庫(kù)了。
CMake 是一個(gè)跨平臺(tái)構(gòu)建系統(tǒng),在 Android Studio 引入 CMake 之前,它就已經(jīng)被廣泛運(yùn)用了。
Google 官方網(wǎng)站上有對(duì) CMake 的使用示范,可以參考 官方指南。
總結(jié)官網(wǎng)對(duì) CMake 的使用,其實(shí)也就如下的步驟:
add_library 指定要編譯的庫(kù),并將所有的
.c或.cpp文件包含指定。include_directories 將頭文件添加到搜索路徑中
set_target_properties 設(shè)置庫(kù)的一些屬性
target_link_libraries 將庫(kù)與其他庫(kù)相關(guān)聯(lián)
如果你對(duì)上面的步驟還是不了解,那么接下來(lái)就更深入了解 CMake 相關(guān)內(nèi)容吧~~~
CMake 的基本操作
以 Clion 作為工具來(lái)講解 CMake 的基本使用。

CMake 編譯可執(zhí)行文件
一個(gè)打印 hello world 的 cpp 文件,通過(guò) CMake 將它編譯成可執(zhí)行文件。
在 cpp 的同一目錄下創(chuàng)建 CMakeLists.txt 文件,內(nèi)容如下:
1# 指定 CMake 使用版本
2cmake_minimum_required(VERSION 3.9)
3# 工程名
4project(HelloCMake)
5# 編譯可執(zhí)行文件
6add_executable(HelloCMake main.cpp )
其中,通過(guò) cmake_minimum_required 方法指定 CMake 使用版本,通過(guò) project 指定工程名。
而 add_executable 就是指定最后編譯的可執(zhí)行文件名稱和需要編譯的 cpp 文件,如果工程很大,有多個(gè) cpp 文件,那么都要把它們添加進(jìn)來(lái)。
定義了 CMake 文件之后,就可以開(kāi)始編譯構(gòu)建了。
CMake 在構(gòu)建工程時(shí)會(huì)生成許多臨時(shí)文件,避免讓這些臨時(shí)文件污染代碼,一般會(huì)把它們放到一個(gè)單獨(dú)的目錄中。
操作步驟如下:
1# 在 cpp 目錄下創(chuàng)建 build 目錄
2mkdir build
3# 調(diào)用 cmake 命令生成 makefile 文件
4cmake ..
5# 編譯
6make
在 build 目錄中可以找到最終生成的可執(zhí)行文件。
這就是 CMake 的一個(gè)簡(jiǎn)單操作,將 cpp 編譯成可執(zhí)行文件,但在 Android 中,大多數(shù)場(chǎng)景都是把 cpp 編譯成庫(kù)文件。
CMake 編譯靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)
同樣還是一個(gè) cpp 文件和一個(gè) CMake 文件,cpp 文件內(nèi)容為打印字符串的函數(shù):
1#include <iostream>
2void print() {
3 std::cout << "hello lib" << std::endl;
4}
同時(shí),CMake 文件也要做相應(yīng)更改:
1cmake_minimum_required(VERSION 3.12)
2# 指定編譯的庫(kù)和文件,SHARED 編譯動(dòng)態(tài)庫(kù)
3add_library(share_lib SHARED lib.cpp)
4# STATIC 編譯靜態(tài)庫(kù)
5# add_library(share_lib STATIC lib.cpp)
通過(guò) add_library 指定要編譯的庫(kù)的名稱,以及動(dòng)態(tài)庫(kù)還是靜態(tài)庫(kù),還有要編譯的文件。
最后同樣地執(zhí)行構(gòu)建,在 build 目錄下可以看到生成的庫(kù)文件。
到這里,就基本可以使用 CMake 來(lái)構(gòu)建 C/C++ 工程了。
CMake 基本語(yǔ)法
熟悉了上面的基本操作之后,就必然會(huì)遇到以下的問(wèn)題了:
如果要參與編譯的 C/C++ 文件很多,難道每個(gè)都要手動(dòng)添加嘛?
可以把編譯好的可執(zhí)行文件或者庫(kù)自動(dòng)放到指定位置嘛?
可以把編譯好的庫(kù)指定版本號(hào)嘛?
帶著這些問(wèn)題,還是要繼續(xù)深入學(xué)習(xí) CMake 的相關(guān)語(yǔ)法,最好的學(xué)習(xí)材料就是 官網(wǎng)文檔 了。
為了避免直接看官方文檔時(shí)一頭霧水,這里列舉一些常用的語(yǔ)法命令。
注釋與大小寫(xiě)
在前面就已經(jīng)用到了 CMake 注釋了,每一行的開(kāi)頭 # 代表注釋。
另外,CMake 的所有語(yǔ)法指令是不區(qū)分大小寫(xiě)的。
變量定義與消息打印
通過(guò) set 來(lái)定義變量:
1# 變量名為 var,值為 hello
2set(var hello)
當(dāng)需要引用變量時(shí),在變量名外面加上 ${} 符合來(lái)引用變量。
1# 引用 var 變量
2${var}
還可以通過(guò) message 在命令行中輸出打印內(nèi)容。
1set(var hello)
2message(${var})
數(shù)學(xué)和字符串操作
數(shù)學(xué)操作
CMake 中通過(guò) math 來(lái)實(shí)現(xiàn)數(shù)學(xué)操作。
1# math 使用,EXPR 為大小
2math(EXPR <output-variable> <math-expression>)
1math(EXPR var "1+1")
2# 輸出結(jié)果為 2
3message(${var})
math 支持 +, -, *, /, %, |, &, ^, ~, <<, >> 等操作,和 C 語(yǔ)言中大致相同。
字符串操作
CMake 通過(guò) string 來(lái)實(shí)現(xiàn)字符串的操作,這波操作有很多,包括將字符串全部大寫(xiě)、全部小寫(xiě)、求字符串長(zhǎng)度、查找與替換等操作。
具體查看 官方文檔。
1set(var "this is string")
2set(sub "this")
3set(sub1 "that")
4# 字符串的查找,結(jié)果保存在 result 變量中
5string(FIND ${var} ${sub1} result )
6# 找到了輸出 0 ,否則為 -1
7message(${result})
8
9# 將字符串全部大寫(xiě)
10string(TOUPPER ${var} result)
11message(${result})
12
13# 求字符串的長(zhǎng)度
14string(LENGTH ${var} num)
15message(${num})
另外,通過(guò)空白或者分隔符號(hào)可以表示字符串序列。
1set(foo this is a list) // 實(shí)際內(nèi)容為字符串序列
2message(${foo})
當(dāng)字符串中需要用到空白或者分隔符時(shí),再用雙括號(hào)""表示為同一個(gè)字符串內(nèi)容。
1set(foo "this is a list") // 實(shí)際內(nèi)容為一個(gè)字符串
2message(${foo})
文件操作
CMake 中通過(guò) file 來(lái)實(shí)現(xiàn)文件操作,包括文件讀寫(xiě)、下載文件、文件重命名等。
具體查看 官方文檔
1# 文件重命名
2file(RENAME "test.txt" "new.txt")
3
4# 文件下載
5# 把文件 URL 設(shè)定為變量
6set(var "http://img.zcool.cn/community/0117e2571b8b246ac72538120dd8a4.jpg")
7
8# 使用 DOWNLOAD 下載
9file(DOWNLOAD ${var} "/Users/glumes/CLionProjects/HelloCMake/image.jpg")
在文件的操作中,還有兩個(gè)很重要的指令 GLOB 和 GLOB_RECURSE 。
1# GLOB 的使用
2file(GLOB ROOT_SOURCE *.cpp)
3# GLOB_RECURSE 的使用
4file(GLOB_RECURSE CORE_SOURCE ./detail/*.cpp)
其中,GLOB 指令會(huì)將所有匹配 *.cpp 表達(dá)式的文件組成一個(gè)列表,并保存在 ROOT_SOURCE 變量中。
而 GLOB_RECURSE 指令和 GLOB 類似,但是它會(huì)遍歷匹配目錄的所有文件以及子目錄下面的文件。
使用 GLOB 和 GLOB_RECURSE 有好處,就是當(dāng)添加需要編譯的文件時(shí),不用再一個(gè)一個(gè)手動(dòng)添加了,同一目錄下的內(nèi)容都被包含在對(duì)應(yīng)變量中了,但也有弊端,就是新建了文件,但是 CMake 并沒(méi)有改變,導(dǎo)致在編譯時(shí)也會(huì)重新產(chǎn)生構(gòu)建文件,要解決這個(gè)問(wèn)題,就是動(dòng)一動(dòng) CMake,讓編譯器檢測(cè)到它有改變就好了。
預(yù)定義的常量
在 CMake 中有許多預(yù)定義的常量,使用好這些常量能起到事半功倍的效果。
CMAKE_CURRENT_SOURCE_DIR
指當(dāng)前 CMake 文件所在的文件夾路徑
CMAKE_SOURCE_DIR
指當(dāng)前工程的 CMake 文件所在路徑
CMAKE_CURRENT_LIST_FILE
指當(dāng)前 CMake 文件的完整路徑
PROJECT_SOURCE_DIR
指當(dāng)前工程的路徑
比如,在 add_library 中需要指定 cpp 文件的路徑,以 CMAKE_CURRENT_SOURCE_DIR 為基準(zhǔn),指定 cpp 相對(duì)它的路徑就好了。
1# 利用預(yù)定義的常量來(lái)指定文件路徑
2add_library( # Sets the name of the library.
3 openglutil
4 # Sets the library as a shared library.
5 SHARED
6 # Provides a relative path to your source file(s).
7 ${CMAKE_CURRENT_SOURCE_DIR}/opengl_util.cpp
8 )
平臺(tái)相關(guān)的常量
CMake 能夠用來(lái)在 Window、Linux、Mac 平臺(tái)下進(jìn)行編譯,在它的內(nèi)部也定義了和這些平臺(tái)相關(guān)的變量。
具體查看 官方文檔 (https://cmake.org/cmake/help/v3.12/manual/cmake-variables.7.html) 。
列舉一些常見(jiàn)的:
WIN32
如果編譯的目標(biāo)系統(tǒng)是 Window,那么 WIN32 為 True 。
UNIX
如果編譯的目標(biāo)系統(tǒng)是 Unix 或者類 Unix 也就是 Linux ,那么 UNIX 為 True 。
MSVC
如果編譯器是 Window 上的 Visual C++ 之類的,那么 MSVC 為 True 。
ANDROID
如果目標(biāo)系統(tǒng)是 Android ,那么 ANDROID 為 1 。
APPLE
如果目標(biāo)系統(tǒng)是 APPLE ,那么 APPLE 為 1 。
有了這些常量做區(qū)分,就可以在一份 CMake 文件中編寫(xiě)不同平臺(tái)的編譯選項(xiàng)。
1if(WIN32){
2 # do something
3}elseif(UNIX){
4 # do something
5}
函數(shù)、宏、流程控制和選項(xiàng) 等命令
具體參考 cmake-commands (https://cmake.org/cmake/help/v3.12/manual/cmake-commands.7.html) ,這里面包括了很多重要且常見(jiàn)的指令。
簡(jiǎn)單示例 CMake 中的函數(shù)操作:
1function(add a b)
2 message("this is function call")
3 math(EXPR num "${a} + $" )
4 message("result is ${aa}")
5endfunction()
6
7add(1 2)
其中,function 為定義函數(shù),第一個(gè)參數(shù)為函數(shù)名稱,后面為函數(shù)參數(shù)。
在調(diào)用函數(shù)時(shí),參數(shù)之間用空格隔開(kāi),不要用逗號(hào)。
宏的使用與函數(shù)使用有點(diǎn)類似:
1macro(del a b)
2 message("this is macro call")
3 math(EXPR num "${a} - $")
4 message("num is ${num}")
5endmacro()
6
7del(1 2)
在流程控制方面,CMake 也提供了 if、else 這樣的操作:
1set(num 0)
2if (1 AND ${num})
3 message("and operation")
4elseif (1 OR ${num})
5 message("or operation")
6else ()
7 message("not reach")
8endif ()
其中,CMake 提供了 AND、OR、NOT、LESS、EQUAL 等等這樣的操作來(lái)對(duì)數(shù)據(jù)進(jìn)行判斷,比如 AND 就是要求兩邊同為 True 才行。
另外 CMake 還提供了循環(huán)迭代的操作:
1set(stringList this is string list)
2foreach (str ${stringList})
3 message("str is ${str}")
4endforeach ()
CMake 還提供了一個(gè) option 指令。
可以通過(guò)它來(lái)給 CMake 定義一些全局選項(xiàng):
1option(ENABLE_SHARED "Build shared libraries" TRUE)
2
3if(ENABLE_SHARED)
4 # do something
5else()
6 # do something
7endif()
可能會(huì)覺(jué)得 option 無(wú)非就是一個(gè) True or False 的標(biāo)志位,可以用變量來(lái)代替,但使用變量的話,還得添加 ${} 來(lái)表示變量,而使用 option 直接引用名稱就好了。
CMake 閱讀實(shí)踐
明白了上述的 CMake 語(yǔ)法以及從官網(wǎng)去查找陌生的指令意思,就基本上可以看懂大部分的 CMake 文件了。
這里舉兩個(gè)開(kāi)源庫(kù)的例子:
https://github.com/g-truc/glm
glm 是一個(gè)用來(lái)實(shí)現(xiàn)矩陣計(jì)算的,在 OpenGL 的開(kāi)發(fā)中會(huì)用到。
CMakeLists.txt 地址在 https://github.com/g-truc/glm/blob/master/CMakeLists.txt
https://github.com/libjpeg-turbo/libjpeg-turbo
libjpeg-turbo 是用來(lái)進(jìn)行圖片壓縮的,在 Android 底層就是用的它。
CMakeLists.txt 地址在 https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/CMakeLists.txt
這兩個(gè)例子中大量用到了前面所講的內(nèi)容,可以試著讀一讀增加熟練度。
為編譯的庫(kù)設(shè)置屬性
接下來(lái)再回到用 CMake 編譯動(dòng)態(tài)庫(kù)的話題上,畢竟 Android NDK 開(kāi)發(fā)也主要是用來(lái)編譯庫(kù)了,當(dāng)編譯完 so 之后,我們可以對(duì)它做一些操作。
通過(guò) set_target_properties 來(lái)給編譯的庫(kù)設(shè)定相關(guān)屬性內(nèi)容,函數(shù)原型如下:
1set_target_properties(target1 target2 ...
2 PROPERTIES prop1 value1
3 prop2 value2 ...)
比如,要將編譯的庫(kù)改個(gè)名稱:
1set_target_properties(native-lib PROPERTIES OUTPUT_NAME "testlib" )
更多的屬性內(nèi)容可以參考官方文檔 (https://cmake.org/cmake/help/v3.9/manual/cmake-properties.7.html#target-properties)。
不過(guò),這里面有一些屬性設(shè)定無(wú)效,在 Android Studio 上試了無(wú)效,在 CLion 上反而可以,當(dāng)然也可能是我使用姿勢(shì)不對(duì)。
比如,實(shí)現(xiàn)動(dòng)態(tài)庫(kù)的版本號(hào):
1set_target_properties(native-lib PROPERTIES VERSION 1.2 SOVERSION 1 )
對(duì)于已經(jīng)編譯好的動(dòng)態(tài)庫(kù),想要把它導(dǎo)入進(jìn)來(lái),也需要用到一個(gè)屬性。
比如編譯的 FFmpeg 動(dòng)態(tài)庫(kù),
1# 使用 IMPORTED 表示導(dǎo)入庫(kù)
2add_library(avcodec-57_lib SHARED IMPORTED)
3# 使用 IMPORTED_LOCATION 屬性指定庫(kù)的路徑
4set_target_properties(avcodec-57_lib PROPERTIES IMPORTED_LOCATION
5 ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jniLibs/armeabi/libavcodec-57.so )
鏈接到其他的庫(kù)
如果編譯了多個(gè)庫(kù),并且想庫(kù)與庫(kù)之間進(jìn)行鏈接,那么就要通過(guò) target_link_libraries 。
1target_link_libraries( native-lib
2 glm
3 turbojpeg
4 log )
在 Android 底層也提供了一些 so 庫(kù)供上層鏈接使用,也要通過(guò)上面的方式來(lái)鏈接,比如最常見(jiàn)的就是 log 庫(kù)打印日志。
如果要鏈接自己編譯的多個(gè)庫(kù)文件,首先要保證每個(gè)庫(kù)的代碼都對(duì)應(yīng)一個(gè) CMakeLists.txt 文件,這個(gè) CMakeLists.txt 文件指定當(dāng)前要編譯的庫(kù)的信息。
然后在當(dāng)前庫(kù)的 CMakeLists.txt 文件中通過(guò) ADD_SUBDIRECTORY 將其他庫(kù)的目錄添加進(jìn)來(lái),這樣才能夠鏈接到。
1ADD_SUBDIRECTORY(src/main/cpp/turbojpeg)
2ADD_SUBDIRECTORY(src/main/cpp/glm)
添加頭文件
在使用的時(shí)候有一個(gè)容易忽略的步驟就是添加頭文件,通過(guò) include_directories 指令把頭文件目錄包含進(jìn)來(lái)。
這樣就可以直接使用 #include "header.h" 的方式包含頭文件,而不用 #include "path/path/header.h" 這樣添加路徑的方式來(lái)包含。
小結(jié)
以上,就是關(guān)于 CMake 的部分總結(jié)內(nèi)容。
歡迎關(guān)注微信公眾號(hào):【音視頻開(kāi)發(fā)進(jìn)階】,獲得最新文章推送~~~
