1 NDK 使用入門
原生開發套件 (NDK) 是一套工具,使您能夠在 Android 應用中使用 C 和 C++ 代碼,並提供衆多平臺庫,您可使用這些平臺庫管理原生 Activity 和訪問物理設備組件,例如傳感器和輕觸輸入。
對於Android初學者NDK不適合,但是若有以下事項,NDK可以派上用場。
- 進一步提升設備性能,以降低延遲,或運行計算密集型應用,如遊戲或物理模擬。
- 重複使用您自己或其他開發者的 C 或 C++ 庫。
流程是:使用NDK將C和CPP代碼編譯到原生庫中,然後使用 Gradle
將原生庫打包到APK中。Java代碼便可以通過Java原生接口(JNI)框架調用原生庫中的函數。
AS編譯原生庫的默認編譯工具是 CMake
。由於很多現有項目是使用 ndk-build
編譯工具包,所以AS也支持 ndk-build,但要創建新的原生庫,則應使用 CMake。
1.1 NDK相關組件:
NDK
: 這套工具使您能在 Android 應用中使用 C 和 C++ 代碼。CMake
: 一款外部編譯工具,可與 Gradle 搭配使用來編譯原生庫。如果使用了 ndk-build,則不需要此組件。LLDB
:Android Studio 用於調試原生代碼的調試程序。
1.2 創建或導入原生項目
Android Studio 設置完成後,可以直接創建支持 C/C++ 的新項目。但如果您要向現有 Android Studio 項目添加或導入原生代碼,則需要按以下基本流程操作:
- 創建新的原生源文件。如果您已經擁有原生代碼或想要導入預編譯原生庫,則可跳過此步驟。
- 創建CMake編譯腳本:告知CMake如何將原生源文件編譯入庫。如果導入和關聯預編譯庫或平臺庫,也需要此編譯腳本。如果現有的原生庫已有
CMakeLists.txt
編譯腳本,或使用 ndk-build 幷包含Android.mk
編譯腳本,則可跳過此步驟。 - 提供一個指向 CMake 或 ndk-build 腳本文件的路徑,將 Gradle 關聯到原生庫。Gradle 使用編譯腳本將源代碼導入您的 Android Studio 項目並將原生庫(SO 文件)打包到 APK 中。
- 通過點擊 Run [編譯並運行應用]。Gradle 會以依賴項的形式添加 CMake 或 ndk-build 進程,用於編譯原生庫並將其隨 APK 一起打包。
2 概念
2.1 簡介
Android NDK是一組可以將C或CPP(原生代碼)嵌入到Android應用中的工具。其對於想執行以下一項或多項操作的開發者特別有用:
- 在平臺之間移植其應用。
- 重複使用現有庫,或者提供其自己的庫供重複使用。
- 在某些情況下提高性能,特別是像遊戲這種計算密集型應用。
2.2 工作原理
2.2.1 主要組件
在編譯應用時,使用如下組件:
- 原生共享庫:NDK從 C/C++ 源代碼編譯這些庫或
.so
文件。 - 原生靜態庫:NDK也可以編譯靜態庫或
.a
文件,而我們可以將靜態庫關聯到其他庫。 - JNI(Java 原生接口):JNI 是 Java 和 C++ 組件用以互相通信的接口。
- ABI(應用二進制藉口):ABI 可以非常精確地定義應用的機器代碼在運行時應該如何與系統交互。NDK 根據這些定義編譯
.so
文件。不同的 ABI 對應不同的架構:NDK 爲 32 位 ARM、AArch64、x86 及 x86-64 提供 ABI 支持。 - 清單:如果編寫的應用不包含 Java 組件,則必須在清單中聲明
NativeActivity
類。
2.2.2 流程
爲Android開發原生應用的一般流程如下:
-
設計應用,確定以 Java 實現的部分,以及需要以原生代碼形式實現的部分。
-
正常地創建一個支持 C/C++ 的Android應用項目。
-
如果要編寫純原生應用,要在
AndroidManifest.xml
中聲明NativeActivity
類。 -
在 “JNI” 目錄中創建一個描述原生庫的
Android.mk
文件,包括名稱、標記、關聯庫和要編譯的源文件。 -
或者,也可以創建一個配置目標 ABI、工具鏈、發佈/調試模式和 STL 的
Application.mk
文件。對於其中任何您未指明的項,將分別使用以下默認值:- ABI:所有非棄用的ABI
- 工具鏈:Clang
- 模式:發佈
- STL:系統
-
將原生源代碼放在項目的
jni
目錄下。 -
使用
ndk-build
編譯原生(.so
,.a
)庫。 -
編譯 Java 組件,生成可執行
.dex
文件。 -
將所有內容封裝到一個 APK 文件中,包括
.so
,.dex
以及應用運行所需的其他文件。
3 JNI
JNI 是指 Java 原生接口。它定義了 Android 從受管理代碼(使用 Java 或 Kotlin 編程語言編寫)編譯的字節碼與原生代碼(使用 C/C++ 編寫)互動的方式。JNI 不依賴於供應商,支持從動態共享庫加載代碼,雖然較爲繁瑣,但有時相當有效。
3.1 常規提示
儘量減少 JNI 層的佔用空間。您需要從幾個方面來考慮實現這一點。您的 JNI 解決方案應該嘗試遵循以下準則(按重要程度依次列出,從最重要的開始):
-
儘可能減少跨 JNI 層編組資源的次數。 跨 JNI 層進行編組的費用十分高昂。嘗試設計一種接口,儘可能減少需要編組的數據量以及必須進行數據編組的頻率。
-
儘可能避免在使用受管理編程語言編寫的代碼與使用 C++ 編寫的代碼之間進行異步通信。這樣可使 JNI 接口更易於維護。通常,您可以使用與界面相同的語言保持異步更新,以簡化異步界面更新。例如,最好使用 Java 編程語言在兩個線程之間進行回調(其中一個線程發出阻塞 C++ 調用,然後在阻塞調用完成時通知界面線程),而不是通過 JNI 從界面線程調用使用 Java 代碼的 C++ 函數。
-
儘可能減少需要接觸 JNI 或被 JNI 接觸的線程數。如果您確實需要以 Java 和 C++ 這兩種語言來利用線程池,請嘗試在池所有者之間(而不是各個工作線程之間)保持 JNI 通信。
-
將接口代碼保存在少量易於識別的 C++ 和 Java 源位置,以便將來進行重構。 請根據需要考慮使用 JNI 自動生成庫。
4 ndk-build
ndk-build
腳本可用於編譯採用 NDK 基於 Make 的編譯系統的項目。
4.1 內部編譯
運行 ndk-build 腳本相當於運行以下命令:
$GNUMAKE -f <ndk>/build/core/build-local.mk <parameters>
$GNUMAKE
指向 GNU Make 3.81 或更高版本,<ndk>
則指向 NDK 安裝目錄。您可以根據這項信息從其他 shell 腳本(甚至是您自己的 Make 文件)中調用 ndk-build。
4.2 從命令行調用
ndk-build
腳本位於 NDK 安裝目錄頂層。要從命令行運行該腳本,請在應用項目目錄或其子目錄中進行調用。例如:
$ cd <project>
$ <ndk>/ndk-build <option>
在此示例中,<project>
指向項目的根目錄,<ndk>
則是您安裝 NDK 的目錄。
-
clean:移除之前生成的所有二進制文件。
-
V=1:啓動編譯,並顯示編譯命令。
-
-B:強制執行完整的重新編譯。
5 Android.mk
5.1 概覽
Android.mk
文件位於項目 jni/
目錄的子目錄中,用於向編譯系統描述源文件和共享庫。它實際上是編譯系統解析一次或多次的微小 GNU makefile 片段。Android.mk
文件用於定義Application.mk
、編譯系統和環境變量所未定義的項目範圍設置。它還可替換特定模塊的項目範圍設置。
Android.mk
的語法支持將源文件分組爲模塊。模塊是靜態庫、共享庫或獨立的可執行文件。可在 Android.mk
文件中定義一個或多個模塊,也可在多個模塊中使用同一個源文件。編譯系統只將共享庫放入您的應用軟件包。此外,靜態庫可生成共享庫。
除了封裝庫之外,編譯系統還可爲您處理各種其他事項。例如,您無需在 Android.mk
文件中列出頭文件或生成的文件之間的顯式依賴關係。NDK 編譯系統會自動計算這些關係。
此文件的語法與隨整個Android 開源項目分發的 Android.mk
文件中使用的語法非常接近。雖然使用這些語法的編譯系統實現並不相同,但通過有意將語法設計得相似,可使應用開發者更輕鬆地將源代碼重複用於外部庫。
5.2 基礎知識
下面來解釋 Android.mk
文件中每一行的作用。
Android.mk
文件必須先定義LOCAL_PATH
變量:
LOCAL_PATH := $(call my-dir)
此變量表示源文件在開發樹中的位置。在這行代碼中,編譯系統提供的宏函數 my-dir
將返回當前目錄(Android.mk
文件本身所在的目錄)的路徑。
- 下一行聲明
CLEAR_VARS
變量,其值由編譯系統提供。
include $(CLEAR_VARS)
CLEAR_VARS
變量指向一個特殊的 GNU Makefile,後者會清除許多 LOCAL_XXX
變量,例如 LOCAL_MODULE
、LOCAL_SRC_FILES
和 LOCAL_STATIC_LIBRARIES
。請注意,GNU Makefile 不會清除 LOCAL_PATH
。此變量必須保留其值,因爲系統在單一 GNU Make 執行環境(其中的所有變量都是全局變量)中解析所有編譯控制文件。在描述每個模塊之前,必須聲明(重新聲明)此變量。
- 接下來,
LOCAL_MODULE
變量存儲要編譯的模塊的名稱。需要在應用的每個模塊中使用一次此變量。
LOCAL_MODULE := hello-jni
每個模塊名稱必須唯一,且不含任何空格。編譯系統在生成最終共享庫文件時,會對您分配給 LOCAL_MODULE
的名稱自動添加正確的前綴和後綴。例如,上述示例會生成名爲 libhello-jni.so
的庫。
-
下一行會列舉源文件,以空格分隔多個文件:
LOCAL_SRC_FILES := hello-jni.c
LOCAL_SRC_FILES
變量必須包含要編譯到模塊中的 C 和/或 C++ 源文件列表。
-
最後一行幫助系統將所有內容連接到一起:
include $(BUILD_SHARED_LIBRARY)
BUILD_SHARED_LIBRARY
變量指向一個 GNU Makefile 腳本,該腳本會收集您自最近 include
以來在 LOCAL_XXX
變量中定義的所有信息。此腳本確定要編譯的內容以及編譯方式。
6 Application.mk
6.1 概覽
Application.mk
指定了 ndk-build 的項目範圍設置。默認情況下,它位於應用項目目錄中的jni/Application.mk
下。
6.2 變量
-
APP_ABI
默認情況下,NDK 編譯系統會爲所有非棄用 ABI 生成代碼。您可以使用
APP_ABI
設置爲特定 ABI 生成代碼。下表顯示了不同指令集的APP_ABI
設置。指令集 值 32 位 ARMv7 APP_ABI := armeabi-v7a
64 位 ARMv8 (AArch64) APP_ABI := arm64-v8a
x86 APP_ABI := x86
x86-64 APP_ABI := x86_64
所有支持的 ABI(默認) APP_ABI := all
您也可以指定多個值,方法是將它們放在同一行上,中間用空格分隔。例如:
APP_ABI := armeabi-v7a arm64-v8a x86
-
APP_PLATFORM
APP_PLATFORM
會聲明編譯此應用所面向的 Android API 級別,並對應於應用的minSdkVersion
。如果未指定,ndk-build 將以 NDK 支持的最低 API 級別爲目標。最新 NDK 支持的最低 API 級別總是足夠低,可以支持幾乎所有使用中的設備。
警告:將
APP_PLATFORM
設置爲高於應用的minSdkVersion
可能會生成一個無法在舊設備上運行的應用。在大多數情況下,庫將無法加載,因爲它們引用了在舊設備上不可用的符號。 -
APP_STL
用於此應用的 C++ 標準庫。
默認情況下使用
system
STL。其他選項包括c++_shared
、c++_static
和none
。
7 CMake
CMake 編譯腳本是一個純文本文件,您必須將其命名爲 CMakeLists.txt
,並在其中包含 CMake 編譯您的 C/C++ 庫時需要使用的命令。如果原生源代碼文件還沒有 CMake 編譯腳本,您需要自行創建一個,並在其中包含適當的 CMake 命令。
7.1 配置 Cmake 編譯腳本
通過添加 CMake 命令來配置您的編譯腳本。要指示 CMake 通過原生源代碼創建原生庫,請將向您的編譯腳本添加 cmake_minimum_required()
和 add_library()
命令:
cmake_minimum_required(VERSION 3.4.1)
add_library( # Specifies 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 )
在使用 add_library()
向 CMake 編譯腳本添加源代碼文件或庫時,Android Studio 還會在您同步項目後在 Project 視圖中顯示相關的頭文件。不過,爲了讓 CMake 能夠在編譯時找到頭文件,您需要向 CMake 編譯文件添加 include_directories()
命令,並指定頭文件的路徑:
add_library(...)
# Specifies a path to native header files.
include_directories(src/main/cpp/include/)
CMake 使用 liblibrary-name.so
規範來爲庫文件命名。
例如,如果您在編譯腳本中指定“native-lib”
作爲共享庫的名稱,CMake 就會創建一個名爲 libnative-lib.so
的文件。不過,在 Java 或 Kotlin 代碼中加載此庫時,請使用您在 CMake 編譯腳本中指定的名稱:
static {
System.loadLibrary("native-lib");
}
Android Studio 會自動向 Project 窗格中的 cpp 組添加源代碼文件和頭文件。通過使用多個 add_library()
命令,您可以爲 CMake 定義要通過其他源代碼文件編譯的更多庫。
7.2 在Gradle中使用CMake變量
要將參數從模塊級 build.gradle
文件傳送到 CMake,請使用以下 (領域特定語言)DSL:
android {
...
defaultConfig {
...
// This block is different from the one you use to link Gradle
// to your CMake build script.
externalNativeBuild {
cmake {
...
// Use the following syntax when passing arguments to variables:
// arguments "-DVAR_NAME=ARGUMENT".
arguments "-DANDROID_ARM_NEON=TRUE",
// If you're passing multiple arguments to a variable, pass them together:
// arguments "-DVAR_NAME=ARG_1 ARG_2"
// The following line passes 'rtti' and 'exceptions' to 'ANDROID_CPP_FEATURES'.
"-DANDROID_CPP_FEATURES=rtti exceptions"
}
}
}
buildTypes {...}
// Use this block to link Gradle to your CMake build script.
externalNativeBuild {
cmake {...}
}
}