Android NDK 開發:CMake 使用

注:首發地址

1. 前言

當在做 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>] (<path-to-source> | <path-to-existing-build>)   

options 為可選項,為空時,構建的路徑為當前路徑。
options 的值,可以通過輸入cmake --help 或到官方文檔CMake-cmake查看,比如:
-G <generator-name> 是指定構建系統的生成器,當前平臺所支持的 generator-name 也可以通過cmake --help查看。(options 一般默認為空就好,這里不做過多介紹)

path-to-sourcepath-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 cmake
brew link cmake
cmake -version #檢驗是否安裝成功,顯示對應 CMake 版本號即表示安裝成功

創建一個 CMake/t1 目錄,并分別編寫 main.c 和 CMakeLists.txt (CMakeLists.txt 是 CMake 的構建定義文件)

#include <stdio.h>
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(<projectname> [CXX] [C] [Java])
這個指令是定義工程名稱的,并且可以指定工程支持的語言(當然也可以忽略,默認情況表示支持所有語言),不是強制定義的。例如:project(HELLO)
定義完這個指令會隱式定義了兩個變量:
<projectname>_BINARY_DIR<projectname>_SOURCE_DIR
由上面的例子也可以看到,MESSAGE 指令有用到這兩個變量;

另外 CMake 系統還會預定義了 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR 變量,它們的值和上面兩個的變量對應的值是一致的。不過為了統一起見,建議直接使用PROJECT_BINARY_DIRPROJECT_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(<VAR> 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_properties
set_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 <debug | optimized> 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 指令會隱式定義<projectname>_BINARY_DIR 和 <projectname>_SOURCE_DIR
而對于顯式定義就是通過 SET 指令來定義。如:set(HELLO_SRC main.c)

7.3 CMake 常用變量

  • 1)CMAKE_BINARY_DIR, PROJECT_BINARY_DIR, <projectname>_BINARY_DIR
    這三個變量指代的內容都是一樣的,如果是 in-source 編譯,指的是工程頂層目錄,如果是 out-of-source 編譯,指的是工程編譯發生的目錄。

  • 2)CMAKE_SOURCE_DIR, PROJECT_SOURCE_DIR, <projectname>_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 指令定義的項目名稱。

介紹了那么多,可以通過一些小練習來鞏固下,參考:cmake 學習筆記(一) - dbzhang800- CSDN博客

代碼地址:NdkSample/CMake Sample

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;靜態庫為STATIC
add_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 的 library
target_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 開發來說,掌握這些也足夠了吧。

參考:

擴展閱讀:

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

推薦閱讀更多精彩內容