Android NDK開發掃盲及最新CMake的編譯使用

1 NDK 簡介

在介紹 NDK 之前還是首推 Android 官方 NDK 文檔。傳送門

官方文檔分別從以下幾個方面介紹了 NDK

  1. NDK 的基礎概念
  2. 如何編譯 NDK 項目
  3. ABI 是什麼以及不同 CPU 指令集支持哪些 ABI
  4. 如何使用您自己及其他預建的庫

本節將會對文檔進行總結和補充。所以建議先瀏覽一遍文檔,或者看完本篇文章再回頭看一遍文檔。

1.1 NDK 基礎概念

首先先用簡單的話分別解釋下 JNI、NDK, 以及分別和 Android 開發、c/c++ 開發的配合。在解釋過程中會對 Android.mk、Application.mk、ndk-build、CMake、CMakeList 這些常見名詞進行掃盲。

  • JNI(Java Native Interface):Java本地接口。是為了方便Java調用c、c++等本地代碼所封裝的一層接口(也是一個標準)。大家都知道,Java的優點是跨平臺,但是作為優點的同時,其在本地交互的時候就編程了缺點。Java的跨平臺特性導致其本地交互的能力不夠強大,一些和操作系統相關的特性Java無法完成,於是Java提供了jni專門用於和本地代碼交互,這樣就增強了Java語言的本地交互能力。上述部分文字摘自任玉剛的 Java JNI 介紹
  • NDK(Native Development Kit) : 原生開發工具包,即幫助開發原生代碼的一系列工具,包括但不限於編譯工具、一些公共庫、開發IDE等。

NDK 工具包中提供了完整的一套將 c/c++ 代碼編譯成靜態/動態庫的工具,而 Android.mk 和 Application.mk 你可以認為是描述編譯參數和一些配置的文件。比如指定使用c++11還是c++14編譯,會引用哪些共享庫,並描述關係等,還會指定編譯的 abi。只有有了這些 NDK 中的編譯工具才能準確的編譯 c/c++ 代碼。

ndk-build 文件是 Android NDK r4 中引入的一個 shell 腳本。其用途是調用正確的 NDK 構建腳本。其實最終還是會去調用 NDK 自己的編譯工具。

那 CMake 又是什麼呢。脫離 Android 開發來看,c/c++ 的編譯文件在不同平臺是不一樣的。Unix 下會使用 makefile 文件編譯,Windows 下會使用 project 文件編譯。而 CMake 則是一個跨平臺的編譯工具,它並不會直接編譯出對象,而是根據自定義的語言規則(CMakeLists.txt)生成 對應 makefile 或 project 文件,然後再調用底層的編譯。

在Android Studio 2.2 之後,工具中增加了 CMake 的支持,你可以這麼認為,在 Android Studio 2.2 之後你有2種選擇來編譯你寫的 c/c++ 代碼。一個是 ndk-build + Android.mk + Application.mk 組合,另一個是 CMake + CMakeLists.txt 組合。這2個組合與Android代碼和c/c++代碼無關,只是不同的構建腳本和構建命令。本篇文章主要會描述後者的組合。(也是Android現在主推的)

1.2 ABI 是什麼

ABI(Application binary interface)應用程序二進制接口。不同的CPU 與指令集的每種組合都有定義的 ABI (應用程序二進制接口),一段程序只有遵循這個接口規範才能在該 CPU 上運行,所以同樣的程序代碼為了兼容多個不同的CPU,需要為不同的 ABI 構建不同的庫文件。當然對於CPU來說,不同的架構並不意味著一定互不兼容。

  • armeabi設備只兼容armeabi;
  • armeabi-v7a設備兼容armeabi-v7a、armeabi;
  • arm64-v8a設備兼容arm64-v8a、armeabi-v7a、armeabi;
  • X86設備兼容X86、armeabi;
  • X86_64設備兼容X86_64、X86、armeabi;
  • mips64設備兼容mips64、mips;
  • mips只兼容mips;

具體的兼容問題可以參見這篇文章。Android SO文件的兼容和適配

當我們開發 Android 應用的時候,由於 Java 代碼運行在虛擬機上,所以我們從來沒有關心過這方面的問題。但是當我們開發或者使用原生代碼時就需要了解不同 ABI 以及為自己的程序選擇接入不同 ABI 的庫。(庫越多,包越大,所以要有選擇)

下面我們來看下一共有哪些 ABI 以及對應的指令集

Android NDK開發掃盲及最新CMake的編譯使用

ABI


2 CMake 的使用

這一節將重點介紹 CMake 的規則和使用,以及如何使用 CMake 編譯自己及其他預建的庫。

2.1 Hello world

我們通過一個Hello World項目來理解 CMake

首先創建一個新的包含原生代碼的項目。在 New Project 時,勾選 Include C++ support

Android NDK開發掃盲及最新CMake的編譯使用

New Project


項目創建好以後我們可以看到和普通Android項目有以下4個不同。

  1. main 下面增加了 cpp 目錄,即放置 c/c++ 代碼的地方
  2. module-level 的 build.gradle 有修改
  3. 增加了 CMakeLists.txt 文件
  4. 多了一個 .externalNativeBuild 目錄
Android NDK開發掃盲及最新CMake的編譯使用

Difference


build.gradle

<code>android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-frtti -fexceptions"
                arguments "-DANDROID_ARM_NEON=TRUE"
            }
        }
    }
    buildTypes {
        ...
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}
...
/<code>

由於 CMake 的命令集成在了 gradle - externalNativeBuild 中,所以在 gradle 中有2個地方配置 CMake。

defaultConfig外面的 externalNativeBuild - cmake,指明瞭 CMakeList.txt 的路徑; defaultConfig 裡面的 externalNativeBuild - cmake,主要填寫 CMake 的命令參數。即由 arguments 中的參數最後轉化成一個可執行的 CMake 的命令,可以在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_command.txt 中查到。如下

Android NDK開發掃盲及最新CMake的編譯使用

cmake command


更多的可以填寫的命令參數和含義可以參見Android NDK-CMake文檔

CMakeLists.txt

CMakeLists.txt 中主要定義了哪些文件需要編譯,以及和其他庫的關係等。

看下新項目中的 CMakeLists.txt

<code>cmake_minimum_required(VERSION 3.4.1)

# 編譯出一個動態庫 native-lib,源文件只有 src/main/cpp/native-lib.cpp
add_library( # Sets the name of the library.
             native-lib
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

# 找到預編譯庫 log_lib 並link到我們的動態庫 native-lib中
find_library( # Sets the name of the path variable.
              log-lib
              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )
target_link_libraries( # Specifies the target library.
                       native-lib
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
/<code>

這其實是一個最基本的 CMakeLists.txt ,其實 CMakeLists.txt 裡面可以非常強大,比如自定義命令、查找文件、頭文件包含、設置變量等等。建議結合 CMake 的官方文檔使用。同時在這推薦一箇中文翻譯的簡易的CMake手冊

2.2 CMake 使用自己及其他預建的庫

當你需要引入已有的靜態庫/動態庫(FFMpeg)或者自己編譯核心部分並提供出去時就需要考慮如何在 CMake 中使用自己及其他預建的庫。

Android NDK 官網的使用現有庫的文檔中還是使用 ndk-build + Android.mk + Application.mk 組合的說明文檔。(其實官方文檔中大部分都是的,並沒有使用 CMake)

幸運的是, Github上的官方示例 裡面有個項目 hello-libs 實現瞭如何創建出靜態庫/動態庫,並引用它。現在我們把代碼拉下來看下具體是如何實現的。

Android NDK開發掃盲及最新CMake的編譯使用

hello-libs


我們先看下Github上的README介紹:

  • app - 從 $project/distribution/ 中使用一個靜態庫和一個動態庫
  • gen-libs - 生成一個動態庫和一個靜態庫並複製到 $project/distribution/ 目錄,你不需要再編譯這個庫,二進制文件已經保存在了項目中。當然,如果有需要你也可以編譯自己的源碼,只需要去掉 setting.gradle 和 app/build.gradle 中的註釋,然後執行一次,接著註釋回去,防止在 build 的過程中不受影響。

我們採用自底向上的方式分析模塊,先看下 gen-libs 模塊。

gen-libs/build.gradle

<code>android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                arguments '-DANDROID_PLATFORM=android-9',
                          '-DANDROID_TOOLCHAIN=clang'
                // explicitly build libs
                targets 'gmath', 'gperf'
            }
        }
    }
    ...
}
...
/<code>

查詢文檔可以知道 arguments 中 -DANDROID_PLATFORM 代表編譯的 android 平臺,文檔建議直接設置 minSdkVersion 就行了,所以這個參數可忽略。另一個參數 -DANDROID_TOOLCHAIN=clang,CMake 一共有2種編譯工具鏈 - clang 和 gcc,gcc 已經廢棄,clang 是默認的。

targets 'gmath', 'gperf' 代表編譯哪些項目。(不填就是都編譯)

cpp/CMakeLists.txt

<code>cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

set(lib_src_DIR ${CMAKE_CURRENT_SOURCE_DIR})

set(lib_build_DIR $ENV{HOME}/tmp)
file(MAKE_DIRECTORY ${lib_build_DIR})

add_subdirectory(${lib_src_DIR}/gmath ${lib_build_DIR}/gmath)
add_subdirectory(${lib_src_DIR}/gperf ${lib_build_DIR}/gperf)
/<code>

外層的 CMakeLists 裡面核心就是 add_subdirectory,查詢CMake 官方文檔 可以知道這條命令的作用是為構建添加一個子路徑。子路徑中的 CMakeLists.txt 也會被執行。即會去分別執行 gmath 和 gperf 中的 CMakeLists.txt

cpp/gmath/CMakeLists.txt

<code>cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

add_library(gmath STATIC src/gmath.c)

# copy out the lib binary... need to leave the static lib around to pass gradle check
set(distribution_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../distribution)
set_target_properties(gmath
                      PROPERTIES
                      ARCHIVE_OUTPUT_DIRECTORY
                      "${distribution_DIR}/gmath/lib/${ANDROID_ABI}")

# copy out lib header file...
add_custom_command(TARGET gmath POST_BUILD
                   COMMAND "${CMAKE_COMMAND}" -E
                   copy "${CMAKE_CURRENT_SOURCE_DIR}/src/gmath.h"
                   "${distribution_DIR}/gmath/include/gmath.h"
#                   **** the following 2 lines are for potential future debug purpose ****
#                   COMMAND "${CMAKE_COMMAND}" -E
#                   remove_directory "${CMAKE_CURRENT_BINARY_DIR}"
                   COMMENT "Copying gmath to output directory")
/<code>

這個是其中一個靜態庫的 CMakeLists.txt,另一個跟他很像。只是把 STATIC 改成了 SHARED (動態庫)。

add_library(gmath STATIC src/gmath.c) 之前用到過,編譯出一個靜態庫,源文件是 src/gmath.c

set_target_properties 命令的意思是設置目標的一些屬性來改變它們構建的方式。這個命令中設置了 gmath 的 ARCHIVE_OUTPUT_DIRECTORY 屬性。也就是改變了輸出路徑。

add_custom_command 命令是自定義命令。命令中把頭文件也複製到了 distribution_DIR 中。

以上就是一個靜態庫/動態庫的編譯過程。總結以下3點

  1. 編譯靜態庫/動態庫
  2. 修改輸出路徑
  3. 複製暴露的頭文件

接著,我們看下 app 模塊是如何使用預建好的靜態庫/動態庫的。

app/src/main/cpp/CMakeLists.txt

<code>cmake_minimum_required(VERSION 3.4.1)

# configure import libs
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../distribution)

# 創建一個靜態庫 lib_gmath 直接引用libgmath.a
add_library(lib_gmath STATIC IMPORTED)
set_target_properties(lib_gmath PROPERTIES IMPORTED_LOCATION
    ${distribution_DIR}/gmath/lib/${ANDROID_ABI}/libgmath.a)

# 創建一個動態庫 lib_gperf 直接引用libgperf.so
add_library(lib_gperf SHARED IMPORTED)
set_target_properties(lib_gperf PROPERTIES IMPORTED_LOCATION
    ${distribution_DIR}/gperf/lib/${ANDROID_ABI}/libgperf.so)

# build application's shared lib
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")

# 創建庫 hello-libs
add_library(hello-libs SHARED
            hello-libs.cpp)

# 加入頭文件
target_include_directories(hello-libs PRIVATE
                           ${distribution_DIR}/gmath/include
                           ${distribution_DIR}/gperf/include)

# hello-libs庫鏈接上 lib_gmath 和 lib_gperf
target_link_libraries(hello-libs
                      android
                      lib_gmath
                      lib_gperf
                      log)
/<code>

我將解釋放在了註釋中。可以看下基本上分成了4個步驟引入:

  1. 分別創建靜態庫/動態庫,直接引用已經有的 .a 文件 或者 .so 文件
  2. 創建自己應用的庫 hello-libs
  3. 加入之前暴露頭文件
  4. 鏈接上靜態庫/動態庫

還是很好理解的。編輯好並 Sync 後,你就可以發現 hello-libs 中的c/c++代碼可以引用暴露的頭文件調用內部方法了。

3 資料文獻

首推 Android NDK 官方文檔,雖然很多都不完整,但是絕對是必須看一遍的東西。

當初次接觸 NDK 開發又覺得新建的 Hello World 項目過於簡單時。建議把 googlesamples - android-ndk 項目拉下來。裡面有多個實例參考,比官方文檔完整很多。

Android NDK開發掃盲及最新CMake的編譯使用

Google Samples


當你發現示例裡的一些NDK配置滿足不了你的需求後,你就需要到 CMake 官方文檔 去查詢完整的支持的函數,同時這裡也提供一箇中文翻譯的簡易的CMake手冊。

以上文檔資料僅為了解決 NDK 開發過程中編譯配置問題,具體 c/c++ 的邏輯編寫、jni等不在此範疇。

彩蛋

文末獻上一組彩蛋,將 CMake 或者 NDK 開發過程中遇到的坑和小技巧以 Q&A 的方式列出。持續更新

Q1:怎麼指定 C++標準?

A:在 build_gradle 中,配置 cppFlags -std

<code>externalNativeBuild {
  cmake {
    cppFlags "-frtti -fexceptions -std=c++14"
    arguments '-DANDROID_STL=c++_shared'
  }
}
/<code>

Q2:add_library 如何編譯一個目錄中所有源文件?

A: 使用 aux_source_directory 方法將路徑列表全部放到一個變量中。

<code># 查找所有源碼 並拼接到路徑列表
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/api SRC_LIST)
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/core CORE_SRC_LIST)
list(APPEND SRC_LIST ${CORE_SRC_LIST})
add_library(native-lib SHARED ${SRC_LIST})
/<code>

Q3:怎麼調試 CMakeLists.txt 中的代碼?

A:使用 message 方法

<code>cmake_minimum_required(VERSION 3.4.1)
message(STATUS "execute CMakeLists")
...
/<code>

然後運行後在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_output.txt 中查看 log。

Q4:什麼時候 CMakeLists.txt 裡面會執行?

A:測試了下,好像在 sync 的時候會執行。執行一次後會生成 makefile 的文件緩存之類的東西放在 externalNativeBuild 中。所以如果 CMakeLists.txt 中沒有修改的話再次同步好像是不會重新執行的。(或者刪除 .externalNativeBuild 目錄)

真正編譯的時候好像只是讀取.externalNativeBuild 目錄中已經解析好的 makefile 去編譯。不會再去執行 CMakeLists.txt

文章轉載自簡書一位博主Tsy遠


分享到:


相關文章: