注:首發地址
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-source
和path-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_DIR
和 PROJECT_SOURCE_DIR
變量,它們的值和上面兩個的變量對應的值是一致的。不過為了統一起見,建議直接使用PROJECT_BINARY_DIR
和PROJECT_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博客
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 開發來說,掌握這些也足夠了吧。
參考:
- CMake 實踐手冊
- CMake語法學習筆記 - 亞特蘭蒂斯 - CSDN博客
- cmake 學習筆記(一) - dbzhang800 - CSDN博客
- 向您的項目添加 C 和 C++ 代碼 | Android Studio 官方文檔
擴展閱讀:
-
Android Studio NDK CMake 指定so輸出路徑以及生成多個so的案例與總結 - zhangbh的專欄 - CSDN博客
如果項目中需要將功能模塊生成不同的 so 庫,可以參考下文章的例子 -
Make 命令教程 - 阮一峰的網絡日志
學習 make 命令可以了解 Makefile 構建規則 -
make makefile cmake qmake都是什么,有什么區別? - 知乎
make用來執行Makefile;Makefile是類unix環境下(比如Linux)的類似于批處理的"腳本"文件;cmake是跨平臺項目管理工具,它用更抽象的語法來組織項目,是一個項目管理工具,是用來執行CMakeLists.txt;qmake是Qt專用的項目管理工具,用來處理*.pro工程文件。Makefile的抽象層次最低,cmake和qmake在Linux等環境下最后還是會生成一個Makefile。cmake和qmake支持跨平臺,cmake的做法是生成指定編譯器的工程文件,而qmake完全自成體系。