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 的部分總結內容。
歡迎關注微信公衆號:【紙上淺談】,得到最新文章推送~~~