在Android開發中,有時候出于安全,性能,代碼共用的考慮,需要使用C/C++撰寫的庫,雖然在現代化工具鏈的支持下,這個作業的難度已經大大降低,但是畢竟萬事開頭難,初學者往往還是會遇到很多不可預測的問題,本篇就是基于此背景下寫的一份簡陋指南,希望能對剛開始撰寫C/C++庫的讀者有所幫助,同時為了盡可能減少認知斷層,本篇將試著從一個最簡單的功能開始,逐步添加工具鏈,直到實作最終功能,真正做到知其然且之所以然,
目標
本篇的目標很簡單,就是能在Android應用中呼叫到C/C++的函式——接收兩個整型值,回傳兩者相加后的值,暫定這個函式為plus
,
從C++源檔案開始
為了從我們最熟悉的地方開始,我們先不用復雜工具,先從最原始的C++源檔案開始.
打開你喜歡的任何一個文本編輯器,VS Code,Notpad++,記事本都行,新建一個文本檔案,并另存為math.cpp
,接下來,就可以在這個檔案中撰寫代碼了.
前面我們的目標已經說得很清楚,實作個plus
函式,接收兩個整型值,回傳兩者之和,所以它可能是下面這樣
int plus(int left,int right)
{
return left + right;
}
我們的源檔案就這樣完成了,是不是很簡單,
但是僅僅有源檔案是不夠的,因為這個只是給人看的,機器看不懂,所以我們就需要第一個工具——編譯器,編譯器能幫我們把人看得懂的轉化成機器也能看得懂的東西,
編譯器
編譯器是個復雜工程,但是都是服務于兩個基本功能
- 理解源檔案的內容(人能看懂的)——檢查出源檔案中的語法錯誤
- 理解二進制的內容(機器能看懂的)——生成二進制的機器碼,
基于這兩個樸素的功能,編譯器卻是撓斷了頭,難點在于功能2,基于這個難點編譯器分成了很多種,常見的像Windows平臺的VS,Linux平臺的G++,Apple的Clang,而對于Android來說,情況略有不同,前面這些編譯器都是運行在特定系統上的,編譯出來的程式通常也只能運行在對應的系統上,以我現在的機器為例,我現在是在Deepin上寫的C++代碼,但是我們的目標是讓代碼跑在Android手機上,是兩個不同的平臺,更悲觀的是,目前為止,還沒有一款可以在手機上運行的編譯器,那我們是不是就不能在手機上運行C++代碼了?當然不是,因為有交叉編譯,
交叉編譯就是在一個平臺上將代碼生成另一個平臺可執行物件的技術,它和普通編譯最大的不同是在鏈接上,因為一般的鏈接直接可以去系統庫找到合適的庫檔案,而交叉編譯不行,因為當前的平臺不是最終運行代碼的平臺,所以交叉編譯還需要有目標平臺的常用庫,當然,這些Google都替我們準備好了,稱為NDK,
NDK
NDK全稱是Native Development Kit,里面有很多工具,編譯器,聯結器,標準庫,共享庫,這些都是交叉編譯必不可少的部分,為了理解方便,我們首先來看看它的檔案結構,以我這臺機器上的版本為例——/home/Andy/Android/Sdk/ndk/21.4.7075529
(Windows上默認位置則是c:\Users\xxx\AppData\Local\Android\Sdk\
), NDK就保存在Sdk目錄下,以ndk
命名,并且使用版本號作為該版本的根目錄,如示例中,我安裝的NDK版本就是21.4.7075529
,同時該示例還是ANDROID_NDK
這個環境變數的值,也就是說,在確定環境變數前,我們需要先確定選用的NDK版本,并且路徑的值取到版本號目錄,
了解了它的存盤位置,接下來我們需要認識兩個重要的目錄
build/cmake/
,這個檔案夾,稍后我們再展開,toolchains/llvm/prebuild/linux-x86_64
,最后的linux-x86_64
根據平臺不同,名稱也不同,如Windows平臺上就是以Windows開頭,但是一般不會找錯,因為這個路徑下就一個檔案夾,并且前面都是一樣的,這里有我們心心念念的編譯器,聯結器,庫,檔案頭等,如編譯器就存在這個路徑下的bin
目錄里,它們都是以clang
和clang++
結尾的,如aarch64-linux-android21-clang++
-
aarch64
代表著這個編譯器能生成用在arm64
架構機器上的二進制檔案,其他對應的還有armv7a
,x86_64
等,不同的平臺要使用相匹配的編譯器,它就是交叉編譯中所說的目標平臺, -
linux
代表我們執行編譯這個操作發生在linux
機器上,它就是交叉編譯中所說的主機平臺, -
android21
這個顯然就是目標系統版本了 -
clang++
代表它是個C++編譯器,對應的C編譯器是clang
,
可以看到,對于Android來說,不同的主機,不同的指令集,不同的Android版本,都對應著一個編譯器,
了解了這么多,終于到激動人性的時刻啦,接下來,我們來編譯一下前面的C++檔案看看,
編譯
通過aarch64-linux-android21-clang++ --help
查看引數,會發現它有很多引數和選項,現在我們只想驗證下我們的C++源檔案有沒有語法錯誤,所以就不管那些復雜的東西,直接一個aarch64-linux-android21-clang++ -c math.cpp
執行編譯,
命令執行完后,假如一切順利,就會在math.cpp
相同目錄下生成math.o
物件檔案,說明我們的原始碼沒有語法錯誤,可進行到下一步的鏈接,
不過,在此之前,先打斷一下,通常我們的專案會包含很多源檔案,參考一些第三方庫,每次都用手工的形式編譯,鏈接顯然是低效且容易出錯的,在工具已經很成熟的現在,我們應該盡量使用成熟的工具,將重心放在我們的業務邏輯上來,CMake
就是這樣的一個工具,
CMake
CMake是個跨平臺的專案構建工具,怎么理解呢?撰寫C++代碼時,有時候需要參考其他目錄的檔案頭,但是在編譯階段,編譯器是不知道該去哪里查找檔案頭的,所以需要一種配置告訴編譯器檔案頭的查找位置,再者,分布在不同目錄的原始碼,需要根據一定的需求打包成不同的庫,又或者,專案中參考了第三方庫,需要在鏈接階段告訴聯結器從哪個位置查找庫,種種這些都是需要配置的東西,
而不同的系統,不同的IDE對于上述配置的支持是不盡相同的,如Windows上的Visual Studio就是需要在專案的屬性里面配置,在開發者使用同樣的工具時,問題還不是很大,但是一旦涉及到多平臺,多IDE的情況,協同開發就會花費大把的時間在配置上,CMake就是為了解決這些問題應運而生的,
CMake的配置資訊都是寫在名為CMakeLists.txt
的檔案中,如前面提到頭檔案參考,原始碼依賴,庫依賴等等,只需要在CmakeLists.txt
中寫一次,就可以在Windows,MacOS,Linux平臺上的主流IDE上無縫使用,如我在Windows的Visual Studio上創建了一個CMake的專案,配置好了依賴資訊,傳給同事,同事用MacOS開發,他可以在一點不修改的情況下,馬上完成編譯,打包,測驗等作業,這就是CMake跨平臺的威力——簡潔,高效,靈活,
使用CMake管理專案
建CMake專案
我們前面已經有了math.cpp
,又有了CMake,現在就把他們結合一下,
怎樣建立一個CMake專案呢?一共分三步:
- 建一個檔案夾
示例中我們就建一個math
的檔案夾吧,
-
在新建的檔案夾里新建
CMakeLists.txt
文本檔案,注意,這里的檔案名不能變, -
在新建的
CMakeLists.txt
檔案里配置專案資訊,
最簡單的CMake專案資訊需要包括至少三個東西
1)、支持的最低CMake版本
cmake_minimum_required(VERSION 3.18,1)
2)、專案名稱
project(math)
3)、生成物——生成物可能是可執行檔案,也可能是庫,因為我們要生成Android上的庫,所以這里是的生成物是庫,
add_library(${PROJECT_NAME} SHARED math.cpp)
經過這三步,CMake專案就建成了,下一步我們來試試用CMake來編譯專案,
編譯CMake專案
在執行真正的編譯前,CMake有個準備階段,這個階段CMake會收集必要的資訊,然后生成滿足條件的工程專案,然后才能執行編譯,
那么什么是必要的資訊呢?CMake為了盡可能降低復雜性,會自己猜測收集一些資訊,
如我們在Windows上執行生成操作,CMake會默認目標平臺就是Windows,默認生成VS的工程,所以在Windows上編譯Windows上的庫就幾乎是零配置的,
-
在
math
目錄下新建一個build
的目錄,然后把作業目錄切換到build
目錄,cd build cmake ..
在命令執行之后,就能在
build
目錄下找到VS的工程,可以直接使用VS打開,無錯誤地完成編譯,當然,更快的方法還是直接使用CMake編譯. -
使用CMake編譯
cmake --build .
注意前面的
..
代表父目錄,也就是CMakeLists.txt
檔案存在的math
目錄,而.
則代表當前目錄,即build
這個目錄,假如這兩步都順利執行了,我們就能在build目錄下識訓一個庫檔案,Windows平臺上可能叫math.dll
,而Linux平臺上可能叫math.so
,但是都是動態庫,因為我們在CMakelists.txt
檔案里配置的就是動態庫,
從上面的流程來看,CMake的作業流程不復雜,但是我們使用的是默認配置,也就是最終生成的庫只能用在編譯的平臺上,要使用CMake編譯Android庫,我們就需要在生成工程時,手動告訴CMake一些配置,而不是讓CMake去猜,
CMake的交叉編譯
配置引數從哪來
雖然我們不知道完成交叉編譯的最少配置是什么,但是我們可以猜一下,
首先要完成原始碼的編譯,編譯器和聯結器少不了,前面也知道了,Android平臺上有專門的編譯器和聯結器,所以至少有個配置應該是告訴CMake用哪一個編譯器和聯結器,
其次Android的系統版本和架構也是必不可少的,畢竟對于Android開發來說,這個對于Android應用都很重要,
還能想到其他引數嗎,好像想不到了,不過,好訊息是,Google替我們想好了,那就是直接使用CMAKE——TOOLCHAIIIN_FILE
,這個選項是CMake 提供的,使用的時候把組態檔路徑設定為它的值就可以了,CMake會通過這個路徑查找到目標檔案,使用目標檔案里面的配置代替它的自己靠猜的引數,而這個組態檔,就是剛才提到過的兩個重要檔案夾之一的build/camke
,我們的組態檔就是該檔案夾下面的android.toolchain.cmake
,
Google的CMake組態檔
android.toolchain.cmake
扮演了一個包裝器的作用,它會利用提供給它的引數,和默認的配置,共同完成CMake的配置作業,其實這個檔案還是個很好的CMake學習資料,可以學到很多CMake的技巧,現在,我們先不學CMake相關的,先來看看我們可用的引數有哪些,在檔案的開頭,Google就把可配置的引數都列舉出來了
ANDROID_TOOLCHAIN
ANDROID_ABI
ANDROID_PLATFORM
ANDROID_STL
ANDROID_PIE
ANDROID_CPP_FEATURES
ANDROID_ALLOW_UNDEFINED_SYMBOLS
ANDROID_ARM_MODE
ANDROID_ARM_NEON
ANDROID_DISABLE_FORMAT_STRING_CHECKS
ANDROID_CCACHE
這些引數其實不是CMake的引數,在組態檔被執行的程序中,這些引數會被轉換成真正的CMake引數,我們可以通過指定這些引數的值,讓CMake完成不同的構建需求,假如都不指定,則會使用默認值,不同的NDK版本,默認值可能會不一樣,
我們來著重看看最關鍵的ANDROID_ABI
和ANDROID_PLATFORM
,前面這個是指當前構建的包運行的CPU指令集是哪一個,可選的值有arneabi-v7a
,arn64-v8a
,x86
,x86_64
,mips
,mips64
,后一個則是指構建包的Android版本,它的值有兩種形式,一種就是直接android-[version]
的形式[version]
在使用時替換成具體的系統版本,如android-23
,代表最低支持的系統版本是Android 23,另一種形式是字串latest
,這個值就如這個單詞的意思一樣,用最新的,
那么我們怎么知道哪個引數可以取哪些值呢,有個簡單方法:先在檔案頭確定要查看的引數,然后全域搜索,看set
和if
相關的陳述句就能確定它支持的引數形式了,
使用組態檔完成交叉編譯
說了那么一大堆,回到最開始的例子上來,現在我們有了CMakelists.txt
,還有了math.cpp
,又找到了針對Android的組態檔android.toolchin.cmake
,那么怎樣才能把三者結合起來呢,這就不得不提到CMake的引數配置了,
在前面,我們直接使用
cmake ..
就完成了工程檔案的生成配置,但是其實它是可以傳遞引數的,CMake的引數都是以-D
開頭,用空白符分割的鍵值對,而CMake預設的引數都是以CMAKE
為開頭的,所以大部分情況下引數的形式都是-DCMAKE_XXX
這種,如給CMake傳遞toolchain
檔案的形式就是
cmake -DCMAKE_TOOLCHAIN_FILE=/home/Andy/Android/Sdk/ndk/21.4.7075529/build/cmake/android.toolchain.cmake
這個引數的意思就是告訴CMake,使用=
后面指定的檔案來配置CMake的引數,
然而,完成交叉編譯,我們還少一個選項——-G
,這個選項是交叉編譯必需的,因為交叉編譯CMake不知道該生成什么形式的工程,所以需要使用這個選項指定生成工程的型別,一種是傳統形式的Make工程,指定形式是
cmake -G "Unix Makefiles"
可以看出,這種形式是基于Unix平臺下的Make工程的,它使用make
作為構建工具,所以指定這種形式以后,還需要指定make
的路徑,工程才能順利完成編譯,而另一種Google推薦的方式是Ninja
,這種方式更簡單,因為不需要單獨指定Ninja
的路徑,它默認就隨CMake安裝在同一個目錄下,所以可以減少一個傳參,Ninja
也是一種構建工具,但是專注速度,所以我們這一次就使用Ninja
,它的指定方式是這樣的
cmake -G Ninja
結合以上兩個引數,就可以得到最終的編譯命令
cmake -GNinja -DCMAKE_TOOLCHAIN_FILE=/home/Andy/Android/Sdk/ndk/21.4.7075529/build/cmake/android.toolchain.cmake ..
生成工程后再執行編譯
cmake --build .
我們就得到了最終能運行在Android上的動態庫了,用我這個NDK版本編譯出來的動態庫支持的Android版本是21,指令集是armeabi-v7a,當然根據前面的描述我們可以像前面傳遞toolchain
檔案一下傳遞期望的引數,如以最新版的Android版本構建x86
的庫,就可以這樣寫
cmake -GNinja -DCMAKE_TOOLCHAIN_FILE=/home/Andy/Android/Sdk/ndk/21.4.7075529/build/cmake/android.toolchain.cmake -DANDROID_PLATFORM=latest -DANDROID_ABI=x86 ..
這就給我們個思路,假如有些第三方庫沒有提供編譯指南,但是是用CMake管理的,我們就可以直接套用上面的公式來編譯這個第三方庫,
JNI
前面在CMake的幫助下,我們已經得到了libmath.so
動態庫,但是這個庫還是不能被Android應用直接使用,因為Android應用是用Java(Kotlin)語言開發的,而它們都是JVM語言,代碼都是跑在JVM上的,要想使用這個庫,還需要想辦法讓庫加載到JVM中,然后才有可能訪問得到,它碰巧的是,JVM還真有這個能力,它就是JNI,
JNI基本思想
JNI能提供Java到C/C++的雙向訪問,也就是可以在Java代碼里訪問C/C++的方法或者資料,反過來也一樣支持,這程序中JVM功不可沒,所以要理解JNI技術,需要我們以JVM的角度思考問題,
JVM好比一個貨物集散中心,無論是去哪個地方的貨物都需要先來到這個集散中心,再通過它把貨物分發到目的地,這里的貨物就可以是Java方法或者C/C++函式,但是和普通的快遞不一樣的是,這里的貨物不知道自己的目的地是哪里,需要集散中心自己去找,那么找的依據從哪里來呢,也就是怎樣保證集散中心查找結果的唯一性呢,最簡單的方法當然就是貨物自己標識自己,并且保證它的唯一性,
顯然對于Java來說,這個問題很好解決,Java有著層層保證唯一性的機制,
- 包名可以保證類名的唯一性;
- 類名可以保證同一包名下類的唯一性;
- 同一個類下可以用方法名保證唯一性;
- 方法發生多載的時候可以用引數型別和個數確定類的唯一性,
而對于C/C++來說,沒有包名和類名,那么用方法名和方法引數可以確定唯一性嗎?答案是可以,只要我們把包名和類名作為一種限定條件,
而添加限定條件的方式有兩種,一種就是簡單粗暴,直接把包名類名作為函式名的一部分,這樣JVM也不用看其他的東西,直接粗暴地將包名,類名,函式名和引數這些對應起來就能確定對端對應的方法了,這種方法叫做靜態注冊,其實這和Android里面的廣播特別像:廣播的靜態注冊就是直接粗暴地在AndroidManifest
檔案中寫死了,不用在代碼里配置,一寫了就生效,對應于靜態注冊,肯定還有個動態注冊的方法,動態注冊就是用寫代碼的方式告訴JVM函式間的對應關系,而不是讓它在函式呼叫時再去查找,顯然這種方式的優勢就是呼叫速度更快一點,畢竟我們只需要一次注冊,就可以在后續呼叫中直接訪問到對端,不再需要查找操作,但是同樣和Android中廣播的動態注冊一樣,動態注冊要繁瑣得多,而且動態注冊還要注意把握好注冊時機,不然容易造成呼叫失敗,我們繼續以前面的libmath.so
為例講解,
Java使用本地庫
Java端訪問C/C++函式很簡單,一共分三步:
-
Java呼叫
System.loadLibrary()
方法載入庫System.loadlibrary("math.so");
這里有個值得注意的地方,CMake生成的動態庫是
libmath.so
,但是這里只寫了math.so
,也就是說不需要傳遞lib
這個前綴,這一步執行完后,JVM就知道有個plus
函式了, -
Java宣告一個和C++函式對應的
native
方法,這里對應指的是引數串列和回傳值要保持一致,方法名則可以不一致,public native int nativePlus(int left,int right);
通常,習慣將
native
方法添加native
的前綴, -
在需要的地方直接呼叫這個
native
方法,呼叫方法和普通的Java方法是一致的,傳遞匹配的引數,用匹配的型別接識訓傳值,
把這幾布融合到一個類里面就是這樣
package hongui.me;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import hongui.me.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("me");
}
ActivityMainBinding binding;
private native int nativePlus(int left,int right);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
binding.sampleText.setText("1 + 1 = "+nativePlus(1,1));
}
}
C/C++端引入JNI
JNI其實對于C/C++來說是一層適配層,在這一層主要做函式轉換的作業,不做具體的功能實作,所以,通常來說我們會新建一個源檔案,用來專門處理JNI層的問題,而JNI層最主要的問題當然就是前面提到的方法注冊問題了,
靜態注冊
靜態注冊的基本思路就是根據現有的Java native
方法寫一個與之對應的C/C++函式簽名,具體來說分四步,
- 先寫出和Java
native
函式一模一樣的函式簽名
int nativePlus(int left,int right)
- 在函式名前面添加包名和類名,因為包名在Java中是用
.
分割的,而C/C++中點通常是用作函式呼叫,為了避免編譯錯誤,需要把.
替換成_
,
hongui_me_MainActivity_nativePlus(int left,int right)
- 轉換函式引數,前面提到過所有的操作都是基于JVM的,在Java中,這些是自然而然的,但是在C/C++中就沒有JVM環境,提供JVM
環境的形式就只能是添加引數,為了達到這個目的,任何JNI的函式都要在引數串列開頭添加兩個引數,而Java里面的最小環境是執行緒,所以第一個引數就是代表呼叫這個函式時,呼叫方的執行緒環境物件JNIEnv
,這個物件是C/C++訪問Java的唯一通道,第二個則是呼叫物件,因為Java中不能直接呼叫方法,需要通過類名或者某個類來呼叫方法,第二個引數就代表那個物件或者那個類,它的型別是jobjet
,從第三個引數開始,引數串列就和Java端一一對應了,但是也只是對應,畢竟有些型別在C/C++端是沒有的,這就是JNI中的型別系統了,對于我們當前的例子來說Java里面的int
值對應著JNI里面的jint
,所以后兩個引數都是jint
型別,這一步至關重要,任何一個引數轉換失敗都可能造成程式崩潰,
hongui_me_MainActivity_nativePlus(
JNIEnv* env,
jobject /* this */,
jint left,
jint right)
- 添加必要前綴,這一步會很容易被忽略,因為這一部分不是那么自然而然,首先我們的函式名還得加一個前綴
Java
,現在的函式名變成了這樣Java_hongui_me_MainActivity_nativePlus
,其次在回傳值兩頭需要添加JNIEXPORT
和JNICALL
,這里回傳值是jint
,所以添加完這兩個宏之后是這樣JNIEXPORT jint JNICALL
,最后還要在最開頭添加extern "C"
的兼容指令,至于為啥要添加這一步,感興趣的讀者可以去詳細了解,簡單概括就是這是JNI的規范,
經過這四步,最終靜態方法找函式的C/C++函式簽名變成了這樣
#include "math.h"
extern "C" JNIEXPORT jint JNICALL
Java_hongui_me_MainActivity_nativePlus(
JNIEnv* env,
jobject /* this */,
jint left,
jint right){
return plus(left,right);
}
注意到,這里我把前面的math.cpp
改成了math.h
,并在JNI適配檔案(檔案名是native_jni.cpp
)中呼叫了這個函式,所以現在有兩個源檔案了,需要更新一下CMakeList.txt
,
cmake_minimum_required(VERSION 3.18,1)
project(math)
add_library(${PROJECT_NAME} SHARED native_jni.cpp)
可以看到這里我們只把最后一行的檔案名改了,因為CMakeLists.txt
當前所在的目錄也是include
的查找目錄,所以不需要給它單獨設定值,假如需要添加其他位置的頭檔案則可以使用include_directories(dir)
添加,
現在使用CMake重新編譯,生成動態庫,這次Java就能直接不報錯運行了,
動態注冊
前面提到過動態注冊需要注意注冊時機,那么什么算是好時機呢?在前面Java使用本地庫這一節,我們知道,要想使用庫,必須先載入,載入成功后就可以呼叫JNI方法了,那么動態注冊必然要發生在載入之后,使用之前,JNI很人性化的想到了這一點,在庫載入完成以后會馬上呼叫jint JNI_OnLoad(JavaVM *vm, void *reserved)
這個函式,這個方法還提供了一個關鍵的JavaVM
物件,簡直就是動態注冊的最佳入口了,確定了注冊時機,現在我們來實操一下,注意:動態注冊和靜態注冊都是C/C++端實作JNI函式的一種方式,同一個函式一般只采用一種注冊方式,所以,接下來的步驟是和靜態注冊平行的,并不是先后關系,
動態注冊分六步
- 新建
native_jni.cpp
檔案,添加JNI_OnLoad()
函式的實作,
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
return JNI_VERSION_1_6;
}
這就是這個函式的標準形式和實作,前面那一串都是JNI函式的標準形式,關鍵點在于函式名和引數以及回傳值,要想這個函式在庫載入后自動呼叫,函式名必須是這個,而且引數形式也不能變,并且用最后的回傳值告訴JVM當前JNI的版本,也就是說,這些都是模板,直接搬就行,
- 得到
JNIEnv
物件
前面提到過,所有的JNI相關的操作都是通過JNIEnv
物件完成的,但是現在我們只有個JavaVM
物件,顯然秘訣就在JavaVM
身上,
通過它的GetEnv
方法就可以得到JNIEnv
物件
JNIEnv *env = nullptr;
vm->GetEnv(env, JNI_VERSION_1_6);
- 找到目標類
前面說過,動態注冊和靜態注冊都是要有包名和類名最限定的,只是使用方式不一樣而已,所以動態注冊我們也還是要使用到包名和類名,不過這次的形式又不一樣了,靜態注冊包名類名用_
代替.
,這一次要用/
代替.
,所以我們最終的類形式是hongui/me/MainActivity
,這是一個字串形式,怎樣將它轉換成JNI中的jclass
型別呢,這就該第二步的JNIEnv
出場了,
jclass cls=env->FindClass("hongui/me/MainActivity");
這個cls
物件就和Java里面那個MainActivity
是一一對應的了,有了類物件下一步當然就是方法了,
- 生成JNI函式物件陣列,
因為動態注冊可以同時注冊一個類的多個方法,所以注冊引數是陣列形式的,而陣列的型別是JNINativeMethod
,這個型別的作用就是把Java端的native
方法和JNI方法聯系在一起,怎么做的呢,看它結構,
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
name
對應Java端那個native
的方法名,所以這個值應該是nativePlus
,signature
對應著這個native
方法的引數串列外加函式型別的簽名,
什么是簽名呢,就是型別簡寫,在Java中有八大基本型別,還有方法,物件,類,陣列等,這些東西都有一套對應的字串形式,好比是一張哈希表,鍵是型別的字串表示,值是對應的Java型別,如jint
是真正的JNI型別,它的型別簽名是I
,也就是int
的首字母大寫,
函式也有自己的型別簽名(paramType)returnType
這里的paramType
和returnType
都需要是JNI型別簽名,型別間不需要任何分隔符,
綜上,nativePlus
的型別簽名是(II)I
,兩個整型引數,回傳另一個整型,
fnPtr
正如它名字一樣,它是一個函式指標,值就是我們真正的nativePlus
實作了(這里我們還沒有實作,所以先假定是jni_plus
),
綜上,最終函式物件陣列應該是下面這樣
JNINativeMethod methods[] = {
{"nativePlus","(II)I",reinterpret_cast<void *>(jni_plus)}
};
- 注冊
現在有了代表類的jclass
物件,還有了代表方法的JNINativeMethod
陣列,還有JNIEnv
物件,把它們結合起來就可以完成注冊了
env->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]));
這里第三個引數是代表方法的個數,我們使用了sizeof
操作法得出了所有的methods
的大小,再用sizeof
得出第一個元素的大小,就可以得到methods
的個數,當然,這里直接手動填入1也是可以的,
- 實作JNI函式
在第4步,我們用了個jni_plus
來代表nativePlus
的本地實作,但是這個函式實際上還沒有創建,我們需要在源檔案中定義,現在這個函式名就可以隨便起了,不用像靜態注冊那樣那么長還不能隨便命名,只要保持最終的函式名和注冊時用的那個名字一致就可以了,但是這里還是要加上extern "C"
的前綴,避免編譯器對函式名進行特殊處理,引數串列和靜態注冊完全一致,所以,我們最終的函式實作如下,
#include "math.h"
extern "C" jint jni_plus(
JNIEnv* env,
jobject /* this */,
jint left,
jint right){
return plus(left,right);
}
好了,動態注冊的實作形式也完成了,CMake編譯后你會發現結果和靜態注冊完全一致,所以這兩種注冊方式完全取決于個人喜好和需求,當需要頻繁呼叫native
方法時,我覺得動態注冊是有優勢的,但是假如呼叫次數很少,完全可以直接用靜態注冊,查找消耗完全可以忽略不記,
One more thing
前面我提到CMake是管理C/C++專案的高手,但是對于Android開發來說,Gradle才是YYDS,這一點Google也意識到了,所以gradle的插件上直接提供了CMake和Gradle無縫銜接的絲滑配置,在android
這個構建塊下,可以直接配置CMakeLists.txt
的路徑和版本資訊,
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.20.5'
}
}
這樣,后面無論是修改了C/C++代碼,還是修改了Java代碼,都可以直接點擊運行,gradle會幫助我們編譯好相應的庫并拷貝到最終目錄里,完全不再需要我們手動編譯和拷貝庫檔案了,當然假如你對它的默認行為還不滿意,還可以通過defaultConfig
配置默認行為,它的大概配置可以是這樣
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
externalNativeBuild {
cmake {
cppFlags += "-std=c++1z"
arguments '-DANDROID_STL=c++_shared'
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
}
}
這里cppFlags
是指定C++相關引數的,對應的還有個cFlags
用來指定C相關引數,arguments
則是指定CMake的編譯引數,最后一個就是我們熟悉的庫最終要編譯生成幾個架構包了,我們這里只是生成兩個,
有了這些配置,Android Studio開發NDK完全就像開發Java一樣,都有智能提示,都可以即時編譯,即時運行,縱享絲滑,
總結
NDK開發其實應該分為兩部分,C++開發和JNI開發,
C++開發和PC上的C++開發完全一致,可以使用標準庫,可以參考第三方庫,隨著專案規模的擴大,引入了CMake來管理專案,這對于跨平臺專案來說優勢明顯,還可以無縫銜接到Gradle中,
而JNI開發則更多的是關注C/C++端和Java端的對應關系,每一個Java端的native
方法都要有一個對應的C/C++函式與之對應,JNI提供
靜態注冊和動態注冊兩種方式來完成這一作業,但其核心都是利用包名,類名,函式名,引數串列來確定唯一性,靜態注冊將包名,類名體現在函式名上,動態注冊則是使用類物件,本地方法物件,JNIENV
的注冊方法來實作唯一性,
NDK則是后面的大BOSS,它提供編譯器,聯結器等工具完成交叉編譯,還有一些系統自帶的庫,如log
,z
,opengl
等等供我們直接使用,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/555486.html
標籤:其他
上一篇:Android-JNI開發概論
下一篇:返回列表