Android NDK相關知識總結(轉自Android Developer中文檔)

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 項目添加或導入原生代碼,則需要按以下基本流程操作:

  1. 創建新的原生源文件。如果您已經擁有原生代碼或想要導入預編譯原生庫,則可跳過此步驟。
  2. 創建CMake編譯腳本:告知CMake如何將原生源文件編譯入庫。如果導入和關聯預編譯庫或平臺庫,也需要此編譯腳本。如果現有的原生庫已有 CMakeLists.txt 編譯腳本,或使用 ndk-build 幷包含 Android.mk 編譯腳本,則可跳過此步驟。
  3. 提供一個指向 CMake 或 ndk-build 腳本文件的路徑,將 Gradle 關聯到原生庫。Gradle 使用編譯腳本將源代碼導入您的 Android Studio 項目並將原生庫(SO 文件)打包到 APK 中。
  4. 通過點擊 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開發原生應用的一般流程如下:

  1. 設計應用,確定以 Java 實現的部分,以及需要以原生代碼形式實現的部分

  2. 正常地創建一個支持 C/C++ 的Android應用項目。

  3. 如果要編寫純原生應用,要在 AndroidManifest.xml中聲明 NativeActivity 類。

  4. “JNI” 目錄中創建一個描述原生庫的 Android.mk文件,包括名稱、標記、關聯庫和要編譯的源文件。

  5. 或者,也可以創建一個配置目標 ABI、工具鏈、發佈/調試模式和 STL 的 Application.mk 文件。對於其中任何您未指明的項,將分別使用以下默認值:

    • ABI:所有非棄用的ABI
    • 工具鏈:Clang
    • 模式:發佈
    • STL:系統
  6. 將原生源代碼放在項目的 jni 目錄下。

  7. 使用 ndk-build 編譯原生(.so.a)庫。

  8. 編譯 Java 組件,生成可執行 .dex 文件。

  9. 將所有內容封裝到一個 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_MODULELOCAL_SRC_FILESLOCAL_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++_sharedc++_staticnone


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 {...}
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章