1 NDK 簡介
在介紹 NDK 之前還是首推 Android 官方 NDK 文檔。傳送門
官方文檔分別從以下幾個方面介紹了 NDK
- NDK 的基礎概念
- 如何編譯 NDK 項目
- ABI 是什麼以及不同 CPU 指令集支持哪些 ABI
- 如何使用您自己及其他預建的庫
本節將會對文檔進行總結和補充。所以建議先瀏覽一遍文檔,或者看完本篇文章再回頭看一遍文檔。
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 以及對應的指令集
2 CMake 的使用
這一節將重點介紹 CMake 的規則和使用,以及如何使用 CMake 編譯自己及其他預建的庫。
2.1 Hello world
我們通過一個Hello World項目來理解 CMake
首先創建一個新的包含原生代碼的項目。在 New Project 時,勾選 Include C++ support
項目創建好以後我們可以看到和普通Android項目有以下4個不同。
- main 下面增加了 cpp 目錄,即放置 c/c++ 代碼的地方
- module-level 的 build.gradle 有修改
- 增加了 CMakeLists.txt 文件
- 多了一個 .externalNativeBuild 目錄
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文檔
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 實現瞭如何創建出靜態庫/動態庫,並引用它。現在我們把代碼拉下來看下具體是如何實現的。
我們先看下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點
- 編譯靜態庫/動態庫
- 修改輸出路徑
- 複製暴露的頭文件
接著,我們看下 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個步驟引入:
- 分別創建靜態庫/動態庫,直接引用已經有的 .a 文件 或者 .so 文件
- 創建自己應用的庫 hello-libs
- 加入之前暴露頭文件
- 鏈接上靜態庫/動態庫
還是很好理解的。編輯好並 Sync 後,你就可以發現 hello-libs 中的c/c++代碼可以引用暴露的頭文件調用內部方法了。
3 資料文獻
首推 Android NDK 官方文檔,雖然很多都不完整,但是絕對是必須看一遍的東西。
當初次接觸 NDK 開發又覺得新建的 Hello World 項目過於簡單時。建議把 googlesamples - android-ndk 項目拉下來。裡面有多個實例參考,比官方文檔完整很多。
當你發現示例裡的一些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遠