本篇文章旨在簡介 Android 中
NDK
是什麼以及重點講解最新 Android Studio 編譯工具CMake
的使用 html
在介紹 NDK
以前仍是首推 Android 官方 NDK
文檔。傳送門android
官方文檔分別從如下幾個方面介紹了 NDK
c++
NDK
的基礎概念NDK
項目ABI
是什麼以及不一樣 CPU 指令集支持哪些 ABI
本節將會對文檔進行總結和補充。因此建議先瀏覽一遍文檔,或者看完本篇文章再回頭看一遍文檔。git
首先先用簡單的話分別解釋下 JNI
、NDK
, 以及分別和 Android 開發、c/c++ 開發的配合。在解釋過程當中會對 Android.mk
、Application.mk
、ndk-build
、CMake
、CMakeList
這些常見名詞進行掃盲。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.mk
和 Application.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
)生成 對應 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如今主推的)
ABI
(Application binary interface)應用程序二進制接口。不一樣的CPU 與指令集的每種組合都有定義的 ABI
(應用程序二進制接口),一段程序只有遵循這個接口規範才能在該 CPU 上運行,因此一樣的程序代碼爲了兼容多個不一樣的CPU,須要爲不一樣的 ABI
構建不一樣的庫文件。固然對於CPU來講,不一樣的架構並不意味着必定互不兼容。
具體的兼容問題能夠參見這篇文章。Android SO文件的兼容和適配
當咱們開發 Android 應用的時候,因爲 Java 代碼運行在虛擬機上,因此咱們歷來沒有關心過這方面的問題。可是當咱們開發或者使用原生代碼時就須要瞭解不一樣 ABI
以及爲本身的程序選擇接入不一樣 ABI
的庫。(庫越多,包越大,因此要有選擇)
下面咱們來看下一共有哪些 ABI
以及對應的指令集
這一節將重點介紹 CMake
的規則和使用,以及如何使用 CMake
編譯本身及其餘預建的庫。
咱們經過一個Hello World項目來理解 CMake
首先建立一個新的包含原生代碼的項目。在 New Project 時,勾選 Include C++ support
項目建立好之後咱們能夠看到和普通Android項目有如下4個不一樣。
main
下面增長了 cpp
目錄,即放置 c/c++ 代碼的地方build.gradle
有修改CMakeLists.txt
文件.externalNativeBuild
目錄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
中查到。以下
更多的能夠填寫的命令參數和含義能夠參見Android NDK-CMake文檔
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手冊
當你須要引入已有的靜態庫/動態庫(FFMpeg)或者本身編譯核心部分並提供出去時就須要考慮如何在 CMake
中使用本身及其餘預建的庫。
Android NDK 官網的使用現有庫的文檔中仍是使用 ndk-build
+ Android.mk
+ Application.mk
組合的說明文檔。(其實官方文檔中大部分都是的,並無使用 CMake
)
幸運的是, Github上的官方示例 裏面有個項目 hello-libs 實現瞭如何建立出靜態庫/動態庫,並引用它。如今咱們把代碼拉下來看下具體是如何實現的。
咱們先看下Github上的README介紹:
$project/distribution/
中使用一個靜態庫和一個動態庫$project/distribution/
目錄,你不須要再編譯這個庫,二進制文件已經保存在了項目中。固然,若是有須要你也能夠編譯本身的源碼,只須要去掉 setting.gradle
和 app/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=clang
,CMake
一共有2種編譯工具鏈 - clang
和 gcc
,gcc
已經廢棄,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
也會被執行。即會去分別執行 gmath
和 gperf
中的 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
命令的意思是設置目標的一些屬性來改變它們構建的方式。這個命令中設置了 gmath
的 ARCHIVE_OUTPUT_DIRECTORY
屬性。也就是改變了輸出路徑。
add_custom_command
命令是自定義命令。命令中把頭文件也複製到了 distribution_DIR
中。
以上就是一個靜態庫/動態庫的編譯過程。總結如下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個步驟引入:
hello-libs
仍是很好理解的。編輯好並 Sync
後,你就能夠發現 hello-libs
中的c/c++代碼能夠引用暴露的頭文件調用內部方法了。
首推 Android NDK 官方文檔,雖然不少都不完整,可是絕對是必須看一遍的東西。
當初次接觸 NDK
開發又以爲新建的 Hello World 項目過於簡單時。建議把 googlesamples - android-ndk 項目拉下來。裏面有多個實例參考,比官方文檔完整不少。
當你發現示例裏的一些NDK配置知足不了你的需求後,你就須要到 CMake 官方文檔 去查詢完整的支持的函數,同時這裏也提供一箇中文翻譯的簡易的CMake手冊。
以上文檔資料僅爲了解決 NDK 開發過程當中編譯配置問題,具體 c/c++ 的邏輯編寫、jni等不在此範疇。
文末獻上一組彩蛋,將 CMake
或者 NDK
開發過程當中遇到的坑和小技巧以 Q&A 的方式列出。持續更新
A:在 build_gradle
中,配置 cppFlags -std
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions -std=c++14"
arguments '-DANDROID_STL=c++_shared'
}
}複製代碼
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})複製代碼
A:使用 message
方法
cmake_minimum_required(VERSION 3.4.1)
message(STATUS "execute CMakeLists")
...複製代碼
而後運行後在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_output.txt
中查看 log。
A:測試了下,好像在 sync 的時候會執行。執行一次後會生成 makefile
的文件緩存之類的東西放在 externalNativeBuild
中。因此若是 CMakeLists.txt
中沒有修改的話再次同步好像是不會從新執行的。(或者刪除 .externalNativeBuild
目錄)
真正編譯的時候好像只是讀取.externalNativeBuild
目錄中已經解析好的 makefile
去編譯。不會再去執行 CMakeLists.txt