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

本篇文章旨在簡介 Android 中 NDK 是什麼以及重點講解最新 Android Studio 編譯工具 CMake 的使用 html

1 NDK 簡介

在介紹 NDK 以前仍是首推 Android 官方 NDK 文檔。傳送門android

官方文檔分別從如下幾個方面介紹了 NDKc++

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

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

1.1 NDK 基礎概念

首先先用簡單的話分別解釋下 JNINDK, 以及分別和 Android 開發、c/c++ 開發的配合。在解釋過程當中會對 Android.mkApplication.mkndk-buildCMakeCMakeList 這些常見名詞進行掃盲。github

JNI(Java Native Interface):Java本地接口。是爲了方便Java調用c、c++等本地代碼所封裝的一層接口(也是一個標準)。你們都知道,Java的優勢是跨平臺,可是做爲優勢的同時,其在本地交互的時候就編程了缺點。Java的跨平臺特性致使其本地交互的能力不夠強大,一些和操做系統相關的特性Java沒法完成,因而Java提供了jni專門用於和本地代碼交互,這樣就加強了Java語言的本地交互能力。上述部分文字摘自任玉剛的 Java JNI 介紹shell

NDK(Native Development Kit) : 原生開發工具包,即幫助開發原生代碼的一系列工具,包括但不限於編譯工具、一些公共庫、開發IDE等。編程

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

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

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

在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設備兼容X8六、armeabi;
  • X86_64設備兼容X86_6四、X8六、armeabi;
  • mips64設備兼容mips6四、mips;
  • mips只兼容mips;

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

當咱們開發 Android 應用的時候,因爲 Java 代碼運行在虛擬機上,因此咱們歷來沒有關心過這方面的問題。可是當咱們開發或者使用原生代碼時就須要瞭解不一樣 ABI 以及爲本身的程序選擇接入不一樣 ABI 的庫。(庫越多,包越大,因此要有選擇)

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

ABI
ABI

2 CMake 的使用

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

2.1 Hello world

咱們經過一個Hello World項目來理解 CMake

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

New Project
New Project

項目建立好之後咱們能夠看到和普通Android項目有如下4個不一樣。

  1. main 下面增長了 cpp 目錄,即放置 c/c++ 代碼的地方
  2. module-level 的 build.gradle 有修改
  3. 增長了 CMakeLists.txt 文件
  4. 多了一個 .externalNativeBuild 目錄

Difference
Difference

build.gradle

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-frtti -fexceptions"
                arguments "-DANDROID_ARM_NEON=TRUE"
            }
        }
    }
    buildTypes {
        ...
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}
...複製代碼

因爲 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 中查到。以下

cmake command
cmake command

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

CMakeLists.txt

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

看下新項目中的 CMakeLists.txt

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} )複製代碼

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

2.2 CMake 使用本身及其餘預建的庫

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

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

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

hello-libs
hello-libs

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

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

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

gen-libs/build.gradle

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                arguments '-DANDROID_PLATFORM=android-9',
                          '-DANDROID_TOOLCHAIN=clang'
                // explicitly build libs
                targets 'gmath', 'gperf'
            }
        }
    }
    ...
}
...複製代碼

查詢文檔能夠知道 arguments-DANDROID_PLATFORM 表明編譯的 android 平臺,文檔建議直接設置 minSdkVersion 就好了,因此這個參數可忽略。另外一個參數 -DANDROID_TOOLCHAIN=clangCMake 一共有2種編譯工具鏈 - clanggccgcc 已經廢棄,clang 是默認的。

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

cpp/CMakeLists.txt

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)複製代碼

外層的 CMakeLists 裏面核心就是 add_subdirectory,查詢CMake 官方文檔 能夠知道這條命令的做用是爲構建添加一個子路徑。子路徑中的 CMakeLists.txt 也會被執行。即會去分別執行 gmathgperf 中的 CMakeLists.txt

cpp/gmath/CMakeLists.txt

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")複製代碼

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

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

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

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

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

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

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

app/src/main/cpp/CMakeLists.txt

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)複製代碼

我將解釋放在了註釋中。能夠看下基本上分紅了4個步驟引入:

  1. 分別建立靜態庫/動態庫,直接引用已經有的 .a 文件 或者 .so 文件
  2. 建立本身應用的庫 hello-libs
  3. 加入以前暴露頭文件
  4. 連接上靜態庫/動態庫

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

3 資料文獻

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

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

Google Samples
Google Samples

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

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

彩蛋

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

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

A:在 build_gradle 中,配置 cppFlags -std

externalNativeBuild {
  cmake {
    cppFlags "-frtti -fexceptions -std=c++14"
    arguments '-DANDROID_STL=c++_shared'
  }
}複製代碼

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

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

# 查找全部源碼 並拼接到路徑列表
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})複製代碼

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

A:使用 message 方法

cmake_minimum_required(VERSION 3.4.1)
message(STATUS "execute CMakeLists")
...複製代碼

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

Q4:何時 CMakeLists.txt 裏面會執行?

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

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

相關文章
相關標籤/搜索