Android NDK 開發:CMake 使用(轉)

轉自:http://cfanr.cn/2017/08/26/Android-NDK-dev-CMake-s-usage/
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 命令行格式有很多種,這裏只介紹一種比較常用的

1
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

1
2
3
brew install cmake
brew link cmake
cmake -version #檢驗是否安裝成功,顯示對應 CMake 版本號即表示安裝成功

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

1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf(“Hello World from CMake!\n”);
return 0;
}

1
2
3
4
5
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 等文件

1
2
3
4
5
-- 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

1
2
3
[ 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( [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: 表示這個庫不被默認構建,除非其他組件依賴或手工構建
    1
    2
    #將compress.c 編譯成 libcompress.so 的共享庫
    add_library(compress SHARED compress.c)

add_library 命令也可以用來導入第三方的庫:
add_library(libname [SHARED | STATIC | MODULE | UNKNOWN] IMPORTED)
如,導入 libjpeg.so

1
add_library(libjpeg SHARED IMPORTED)

導入庫後,當需要使用 target_link_libraries 鏈接庫時,可以直接使用該庫

6.8 find_library 指令

語法:find_library( name1 path1 path2 …)
VAR 變量表示找到的庫全路徑,包含庫文件名 。例如:

1
2
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 動態庫和靜態庫:

1
2
set_target_properties(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
set_target_properties(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

指定要導入的庫文件的路徑

1
2
3
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)
如上面的例子,獲取輸出的庫的名字

1
2
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 library2…)
這個指令可以用來爲 target 添加需要的鏈接的共享庫,同樣也可以用於爲自己編寫的共享庫添加共享庫鏈接。
如:

1
2
#指定 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 指令會隱式定義_BINARY_DIR 和 _SOURCE_DIR
而對於顯式定義就是通過 SET 指令來定義。如:set(HELLO_SRC main.c)

7.3 CMake 常用變量

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

  • 2)CMAKE_SOURCE_DIR, PROJECT_SOURCE_DIR, _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 的配置:( 去掉原有的註釋)

1
2
3
4
5
6
7
8
#設置編譯 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 來做圖片壓縮的項目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 的參數有點複雜,一般設置爲空或不設置也是可以的,這裏就不過多介紹了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 開發來說,掌握這些也足夠了吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章