1. 程式人生 > >Android NDK 開發:CMake 使用

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( [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 PROJECTBINARYDIR/bin)set(LIBRARYOUTPUTPATH{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( 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

#指定 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 指令定義的專案名稱。

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 開發來說,掌握這些也足夠了吧。

參考:

擴充套件閱讀:

  • make makefile cmake qmake都是什麼,有什麼區別? - 知乎
    make用來執行Makefile;Makefile是類unix環境下(比如Linux)的類似於批處理的”指令碼”檔案;cmake是跨平臺專案管理工具,它用更抽象的語法來組織專案,是一個專案管理工具,是用來執行CMakeLists.txt;qmake是Qt專用的專案管理工具,用來處理*.pro工程檔案。Makefile的抽象層次最低,cmake和qmake在Linux等環境下最後還是會生成一個Makefile。cmake和qmake支援跨平臺,cmake的做法是生成指定編譯器的工程檔案,而qmake完全自成體系。