Android NDK 開發之 CMake 必知必會

Android Studio 從 2.2 版本起開始支持 CMake ,可以通過 CMake 和 NDK 將 C/C++ 代碼編譯成底層的庫,然后再配合 Gradle 的編譯將庫打包到 APK 中。

這意味就不需要再編寫 .mk 文件來編譯 so 動態庫了。

CMake 是一個跨平臺構建系統,在 Android Studio 引入 CMake 之前,它就已經被廣泛運用了。

Google 官方網站上有對 CMake 的使用示范,可以參考 官方指南。

總結官網對 CMake 的使用,其實也就如下的步驟:

  1. add_library 指定要編譯的庫,并將所有的 .c.cpp 文件包含指定。
  2. include_directories 將頭文件添加到搜索路徑中
  3. set_target_properties 設置庫的一些屬性
  4. target_link_libraries 將庫與其他庫相關聯

如果你對上面的步驟還是不了解,那么接下來就更深入了解 CMake 相關內容吧~~~

CMake 的基本操作

Clion 作為工具來講解 CMake 的基本使用。

clion_cmake_build

CMake 編譯可執行文件

一個打印 hello world 的 cpp 文件,通過 CMake 將它編譯成可執行文件。

在 cpp 的同一目錄下創建 CMakeLists.txt 文件,內容如下:

# 指定 CMake 使用版本
cmake_minimum_required(VERSION 3.9)
# 工程名
project(HelloCMake)
# 編譯可執行文件
add_executable(HelloCMake main.cpp )

其中,通過 cmake_minimum_required 方法指定 CMake 使用版本,通過 project 指定工程名。

add_executable 就是指定最后編譯的可執行文件名稱和需要編譯的 cpp 文件,如果工程很大,有多個 cpp 文件,那么都要把它們添加進來。

定義了 CMake 文件之后,就可以開始編譯構建了。

CMake 在構建工程時會生成許多臨時文件,避免讓這些臨時文件污染代碼,一般會把它們放到一個單獨的目錄中。

操作步驟如下:

# 在 cpp 目錄下創建 build 目錄
mkdir build
# 調用 cmake 命令生成 makefile 文件
cmake ..
# 編譯
make

在 build 目錄中可以找到最終生成的可執行文件。

這就是 CMake 的一個簡單操作,將 cpp 編譯成可執行文件,但在 Android 中,大多數場景都是把 cpp 編譯成庫文件。

CMake 編譯靜態庫和動態庫

同樣還是一個 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 基本語法

熟悉了上面的基本操作之后,就必然會遇到以下的問題了:

  • 如果要參與編譯的 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")

在文件的操作中,還有兩個很重要的指令 GLOBGLOB_RECURSE

# GLOB 的使用
file(GLOB ROOT_SOURCE *.cpp)
# GLOB_RECURSE 的使用
file(GLOB_RECURSE CORE_SOURCE ./detail/*.cpp)

其中,GLOB 指令會將所有匹配 *.cpp 表達式的文件組成一個列表,并保存在 ROOT_SOURCE 變量中。

GLOB_RECURSE 指令和 GLOB 類似,但是它會遍歷匹配目錄的所有文件以及子目錄下面的文件。

使用 GLOBGLOB_RECURSE 有好處,就是當添加需要編譯的文件時,不用再一個一個手動添加了,同一目錄下的內容都被包含在對應變量中了,但也有弊端,就是新建了文件,但是 CMake 并沒有改變,導致在編譯時也會重新產生構建文件,要解決這個問題,就是動一動 CMake,讓編譯器檢測到它有改變就好了。

預定義的常量

在 CMake 中有許多預定義的常量,使用好這些常量能起到事半功倍的效果。

  • CMAKE_CURRENT_SOURCE_DIR
    • 指當前 CMake 文件所在的文件夾路徑
  • CMAKE_SOURCE_DIR
    • 指當前工程的 CMake 文件所在路徑
  • CMAKE_CURRENT_LIST_FILE
    • 指當前 CMake 文件的完整路徑
  • PROJECT_SOURCE_DIR
    • 指當前工程的路徑

比如,在 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 平臺下進行編譯,在它的內部也定義了和這些平臺相關的變量。

具體查看 官方文檔

列舉一些常見的:

  • WIN32
    • 如果編譯的目標系統是 Window,那么 WIN32 為 True 。
  • UNIX
    • 如果編譯的目標系統是 Unix 或者類 Unix 也就是 Linux ,那么 UNIX 為 True 。
  • MSVC
    • 如果編譯器是 Window 上的 Visual C++ 之類的,那么 MSVC 為 True 。
  • ANDROID
    • 如果目標系統是 Android ,那么 ANDROID 為 1 。
  • APPLE
    • 如果目標系統是 APPLE ,那么 APPLE 為 1 。

有了這些常量做區分,就可以在一份 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} + $" )
    message("result is ${aa}")
endfunction()

add(1 2)

其中,function 為定義函數,第一個參數為函數名稱,后面為函數參數。

在調用函數時,參數之間用空格隔開,不要用逗號。

宏的使用與函數使用有點類似:

macro(del a b)
    message("this is macro call")
    math(EXPR num "${a} - $")
    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 語法以及從官網去查找陌生的指令意思,就基本上可以看懂大部分的 CMake 文件了。

這里舉兩個開源庫的例子:

這兩個例子中大量用到了前面所講的內容,可以試著讀一讀增加熟練度。

為編譯的庫設置屬性

接下來再回到用 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 的部分總結內容。

歡迎關注微信公眾號:【紙上淺談】,獲得最新文章推送~~~

掃碼關注
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • 感賞兒子下午放學回來邀請我看他打一局游戲,而且說話算話,就打了一局,然后又興高采烈地給我講解游戲中各種槍的功能,可...
    燕子重生scy閱讀 184評論 1 3
  • 最近一直熱播的電影《芳華》,讓很多人開始回憶自己的青蔥歲月,也使得很多人在曬自己的18歲,我卻應景的想到今年正好是...
    冰封的轉閱讀 414評論 0 2