Java 作為一種編程語言,具有許多良好的功能,使其成為應(yīng)用程序開發(fā)的首選語言。它獨立于平臺(因為虛擬機(jī)執(zhí)行)、JIT 編譯、多線程支持以及為程序員提供的富有表現(xiàn)力的簡單語法。由于其與平臺無關(guān)的特性,Java 包可以跨 CPU 架構(gòu)移植,這使得庫開發(fā)變得更加容易,從而增強(qiáng)了插件、構(gòu)建工具和實用程序包的整體生態(tài)系統(tǒng)。
功能數(shù)量與性能之間恰好存在權(quán)衡。像匯編這樣的語言具有最少的內(nèi)存和執(zhí)行開銷,但從程序員的角度來看,其功能數(shù)量也最少。在層次結(jié)構(gòu)中向上移動,C 和 C++ 等語言提供了一組良好的功能,同時保持更接近底層硬件。在它們之上的是Java和Python等語言,它們選擇通過使用虛擬機(jī)來完全消除平臺依賴。用這些語言編寫的程序有巨大的開銷,但卻是開發(fā)人員的天堂。
JVM 現(xiàn)在可以訪問共享庫中公開的函數(shù),并且操作系統(tǒng)根據(jù)需要執(zhí)行它們。
一、為什么有人在他們的 Android 項目中需要 C/C++ 支持?
正如我們上面的討論,在我們的系統(tǒng)中,性能比開發(fā)人員友好性更重要,這將我們的重點從 Java/Kotlin 轉(zhuǎn)移到了“原生語言”(C/C++)。讓我們通過一些示例來了解本機(jī)代碼的作用及其性能改進(jìn),
- 圖形、渲染和交互:在 Jetpack Compose 等高級框架中,開發(fā)用戶界面并使其看起來有吸引力似乎是小菜一碟。在像素級別,需要進(jìn)行數(shù)千次計算來計算陰影的強(qiáng)度、閃電模式和物體的紋理。這些計算涉及大量使用線性代數(shù)結(jié)構(gòu),例如向量和矩陣及其各自的運算。處理觸摸交互,包括處理移動屏幕上觸摸傳感器的原始坐標(biāo),以及區(qū)分單擊、雙擊、拖動或滑動手勢,也需要大量計算。這些計算可以用更接近硬件的語言更好地執(zhí)行,從而可以執(zhí)行額外的優(yōu)化。
- 機(jī)器學(xué)習(xí):C/C++ 的作用很容易理解,因為 PyTorch 和 TensorFlow 等流行框架的代碼庫的主要部分是用 C/C++ 編寫的。TensorFlow 使用用 C++ 編寫的操作,并提供包裝器(接口)來使用 Python 代碼中的這些操作。C++ 的采用是顯而易見的,作為線性代數(shù)運算的代碼庫,CUDA(用于并行處理)是多年前編寫的,并且已經(jīng)經(jīng)過多年的考驗。使用Python作為TensorFlow的接口之一,只是為了讓C/C++的東西看起來整潔,并且對于非編程用戶來說也很容易。
圖:TensorFlow 源代碼片段,顯示了 arg_max 操作的實現(xiàn)。圖片參考:作者截圖自GitHub上TensorFlow官方源代碼
許多此類系統(tǒng)在犧牲可讀性和其他一些因素的情況下維護(hù)性能。接下來,我們將簡短討論指令集架構(gòu) (ISA) 以及程序執(zhí)行如何隨著 CPU 架構(gòu)的變化而變化。
二、了解 C++ 如何集成到 Android 應(yīng)用程序中
圖 1:Android 應(yīng)用程序如何使用 C++ 源代碼的描述。
如上圖所示,描述了 Android 中 C/C++ 代碼的使用,其中存在兩個獨立的構(gòu)建過程,一個用于 C/C++ 代碼,另一個用于 Java/Kotlin 代碼。在本博客中,我們將重點關(guān)注 C/C++ 代碼構(gòu)建過程,并了解代碼如何與 JVM 通信以進(jìn)行函數(shù)調(diào)用。
我們首先簡要概述一下 C/C++ 和 Java 程序的編譯方式,主要強(qiáng)調(diào) C/C++ 編譯的平臺特定性。接下來,我們討論 JNI,它充當(dāng) C/C++ 和 Java 代碼之間的粘合劑。我們結(jié)束對 CMake、共享庫和 ABI 的討論,它們是構(gòu)建過程的最底層組件。
正如 Reddit 用戶建議的那樣
pjmlp
,Android 不使用 JVM 在設(shè)備上運行應(yīng)用程序。它有自己的運行時,ART(Android RunTime)繼承了 Dalvik 及其自定義字節(jié)碼 DEX。
讓我們開始吧??
三、C++和Java程序的編譯
?? C++ 是一種編譯語言,源代碼被轉(zhuǎn)換為可執(zhí)行的二進(jìn)制代碼。可執(zhí)行文件包含源程序的二進(jìn)制版本、常量和庫代碼(如果需要)。
?? 該可執(zhí)行文件由操作系統(tǒng)的一個組件(稱為加載程序)解析,該組件為程序的執(zhí)行分配內(nèi)存并從可執(zhí)行文件中讀取指令。例如,如果一個hello-world C++ 程序是g++
在 Ubuntu 上編譯的,那么它也可以在其他一些 Linux 發(fā)行版上運行,只要它們理解x86
或x86_64
指令集。
arm
?? 移動設(shè)備在指令集上運行arm64
,因此編譯的程序x86
將無法工作,因為兩個可執(zhí)行文件都是用完全不同的語言編寫的(如加載程序所見)。
圖 2:C++ 和 Java 程序的編譯。Java 編譯器生成的類文件與平臺無關(guān),而 C++ 編譯器(如 GCC)生成的可執(zhí)行文件(或共享庫)與平臺相關(guān)。Java類文件需要目標(biāo)機(jī)器上的JVM來執(zhí)行程序,而C++可執(zhí)行文件可以直接使用操作系統(tǒng)提供的鏈接器和加載器運行。
一個 Android 項目,包含四種不同平臺/架構(gòu)的庫。(作者截圖)
Android 設(shè)備主要可以在四種架構(gòu)上運行- arm64-v8a
、armeabi-v7a
和x86
。x86_64
該arm-
架構(gòu)還適用于大多數(shù) Android 手機(jī)中使用的基于 ARM 的處理器,而x86-
基于 ARM 的處理器則用于 Intel 或 AMD 處理器,例如 Windows 模擬器和 Chromebook。
3.1 Java
?? 如果您在某個時間點學(xué)習(xí)過 Java,視頻和博客中經(jīng)常強(qiáng)調(diào)的一個顯著功能是平臺獨立性或構(gòu)建一次,隨處運行。Java 不是將源代碼轉(zhuǎn)換為機(jī)器相關(guān)的可執(zhí)行格式,而是將代碼轉(zhuǎn)換為中間表示 (IR)。
x86
?? IR 與平臺無關(guān),這意味著無論指令集有何arm
差異,平臺上生成的 IR都是相同的。IR 由一個依賴于平臺的組件(稱為 Java 虛擬機(jī))進(jìn)行解析,該組件從中讀取指令并在底層 CPU 上執(zhí)行它們。由于 JVM 一只手負(fù)責(zé) IR,另一只手負(fù)責(zé)機(jī)器的 CPU,因此它與平臺無關(guān)。
JVM 支持即時 (JIT) 編譯,與純解釋性語言相比,這種技術(shù)可以提供巨大的性能提升。
?? JVM 可以在幾乎所有 CPU 架構(gòu)上運行,并執(zhí)行在任何平臺上編寫的 Java 代碼(因為生成的 IR 與平臺無關(guān)),唯一的依賴項是我們需要在目標(biāo)計算機(jī)上安裝 JVM。
總而言之,Java 和 C++ 有不同的編譯策略,關(guān)鍵是 C++ 執(zhí)行依賴于體系結(jié)構(gòu),因此如果我們嘗試將 C++ 與任何體系結(jié)構(gòu)中立的語言(如 Java)一起使用,我們需要確保 C++ 依賴關(guān)系尊重不同的編譯策略。他們將在其上運行的架構(gòu)。
3.2 Android ART 和 DEX 字節(jié)碼
Android 作為一個操作系統(tǒng),并不使用標(biāo)準(zhǔn)的 JVM 來執(zhí)行 Java 代碼。打包的應(yīng)用程序 APK 包含 DEX 文件(類似于.class
文件)以及本機(jī)代碼和資源。DEX 文件由操作系統(tǒng)提前(在執(zhí)行之前)編譯為本機(jī)可執(zhí)行代碼,當(dāng)用戶打開應(yīng)用程序時,可以快速實例化這些代碼。
四、使用 JNI 包裝 C++ 源代碼
JNI 或 Java 本機(jī)接口是一個允許 JVM 和本機(jī)代碼(C、C++ 或匯編代碼)之間輕松通信的框架。一般來說,它提供外部函數(shù)接口(FFI),允許用一種語言編寫的代碼與用另一種語言編寫的代碼進(jìn)行通信,通常是通過函數(shù)調(diào)用的方式。Java 源代碼可以搜索 C++ 模塊中存在的函數(shù)定義,這些函數(shù)被標(biāo)記為供 JVM 使用。
// C++ source file
extern "C" JNIEXPORT jstring JNICALL
Java_com_projects_ml_samplecppdemo_MainActivity_compute(
JNIEnv* env,
jobject instance ,
jstring message ,
jlong length
) {
// Method block goes here
}
compute
將在 中具有等效的 Kotlin 函數(shù)MainActivity
,
// Kotlin source file
external fun compute( message: String , length: Long ): String
在編譯時,JVM 需要找到我們在代碼中聲明的MainActivity.kt
函數(shù)的定義。compute
我們知道,定義包含在C++源文件中,那么我們?nèi)绾螌⑵涮峁┙oJava程序呢?我們編譯 C++ 代碼并將其打包為共享庫,JVM 將在其中搜索 JNI 函數(shù)的定義。
五、CMake和Android NDK
CMake和Android NDK在C/C++源代碼編譯中的作用
5.1 Android NDK 和工具鏈
我們在基于 Windows、macOS 或 Linux 的操作系統(tǒng)上開發(fā) Android 應(yīng)用程序。這些系統(tǒng)大多數(shù)沒有 Android 特定的 ARM 架構(gòu),并且無法在 Android 設(shè)備上編譯代碼。那么我們?nèi)绾螢槭謾C(jī)使用的Android特定ARM架構(gòu)編譯代碼呢?
Android NDK 中存在的工具鏈概述。
我們使用 Android NDK(Android 本機(jī)開發(fā)套件),它提供編譯器和鏈接器來從x86
甚至其他arm
設(shè)備(Apple Silicon 或 Raspberry Pi)構(gòu)建 Android-ARM 庫和可執(zhí)行文件。在運行其他目標(biāo)(例如 Android-ARM)的系統(tǒng)上為其他目標(biāo)(例如 Android-ARM x86_64
)構(gòu)建代碼的過程稱為交叉編譯。因此,在 Windows 計算機(jī)上,使用 Android NDK 的編譯器,我們可以為應(yīng)用程序構(gòu)建共享庫,該應(yīng)用程序可以在移動設(shè)備(即 ARM 設(shè)備)上完美運行。
Android NDK 中有一個CMAKE_TOOLCHAIN_FILE
,它通知 CMake 使用哪個編譯器。正如維基百科所說,工具鏈?zhǔn)且唤M編程工具,用于執(zhí)行復(fù)雜的軟件開發(fā)任務(wù)或創(chuàng)建軟件產(chǎn)品,Android NDK為不同的Android API級別提供了各種工具鏈來構(gòu)建和編譯C/C++程序。
5.2 什么是 CMake?
g++
如果我們要編譯一個簡單的 C++ hello-world 程序,我們將使用大多數(shù) Linux 發(fā)行版中預(yù)安裝的GNU編譯器,
g++ main.cpp -o main
?? 對于單個源文件,main.cpp
單個命令即可完成工作。較大的代碼庫可能具有多個模塊和大量 C/C++ 源文件,這些文件必須編譯或構(gòu)建到共享/靜態(tài)庫中。此類代碼庫(其他 C++ 項目)的依賴關(guān)系需要很好地集成。如此龐大的代碼庫也需要大量的編譯時間。
??為了解決這些問題,Make
可以使用GNU的工具,它提供了管理多個目標(biāo)、增量構(gòu)建、包含頭文件的能力以及支持多種語言的功能。因此,單個Make
腳本將有效地執(zhí)行編譯,而不是運行多個命令進(jìn)行編譯。
cmake_minimum_required(VERSION 3.22.1)
project("samplecppdemo")
# Tell CMake to build a shared library (.so) for the given
# source file native-lib.cpp.
# native-lib.cpp also contains the JNI functions
add_library(
${CMAKE_PROJECT_NAME}
SHARED
native-lib.cpp)
# CMake can also link other libraries to the current build
# android and log are used to provide android-specific routines
# and logging respectively
target_link_libraries(
${CMAKE_PROJECT_NAME}
android
log)
?? CMake 可以Make
以獨立于編譯器的方法生成腳本,并具有自己的語法,允許開發(fā)人員添加依賴項、標(biāo)頭和其他必須在編譯時鏈接的庫。CMake 類似于 Gradle,因為兩者都是構(gòu)建系統(tǒng)。
六、共享庫和 ABI
?? C/C++ 代碼的編譯可以生成可執(zhí)行文件或庫,兩者都包含源代碼的二進(jìn)制表示形式。main
可執(zhí)行文件具有其他詳細(xì)信息,例如執(zhí)行開始處的函數(shù)地址并遵循 ELF 格式。庫通過將庫與程序的目標(biāo)代碼鏈接來提供可由其他程序調(diào)用的函數(shù)。
Android 應(yīng)用程序中每個目標(biāo)架構(gòu)的 .so 庫
?? 在 Android 中,C/C++ 文件被編譯為共享庫,以.so
(共享對象)擴(kuò)展名結(jié)尾。這些庫公開了我們在 (2) 中編寫的 JNI 函數(shù),正如它們extern
在原型中所標(biāo)記的那樣。JVM 可以查看.so
文件的代碼并使用函數(shù)的二進(jìn)制代碼在設(shè)備上執(zhí)行它。文章來源:http://www.zghlxwxcb.cn/news/detail-839427.html
?? 這種發(fā)生在二進(jìn)制級別的源代碼和庫代碼之間的交互通常是通過應(yīng)用程序二進(jìn)制接口 (ABI) 發(fā)生的。相反,應(yīng)用程序編程接口 (API) 在編譯發(fā)生之前促進(jìn)源代碼級別的此類交互。文章來源地址http://www.zghlxwxcb.cn/news/detail-839427.html
到了這里,關(guān)于在 Android 中使用 C/C++:初學(xué)者綜合指南的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!