Android-JNI開發概論

来源:https://www.cnblogs.com/honguilee/archive/2023/06/17/17478595.html
-Advertisement-
Play Games

### 什麼是JNI開發 JNI的全稱是Java Native Interface,顧名思義,這是一種解決Java和C/C++相互調用的編程方式。**它其實只解決兩個方面的問題,怎麼找到和怎麼訪問。** 弄清楚這兩個話題,我們就學會了JNI開發。**需要註意的是,JNI開發只涉及到一小部分C/C++ ...


什麼是JNI開發

JNI的全稱是Java Native Interface,顧名思義,這是一種解決Java和C/C++相互調用的編程方式。它其實只解決兩個方面的問題,怎麼找到和怎麼訪問。 弄清楚這兩個話題,我們就學會了JNI開發。需要註意的是,JNI開發只涉及到一小部分C/C++開發知識,遇到問題的時候我們首先要判斷是C/C++的問題還是JNI的問題,這可以節省很多搜索和定位的時間。

用JVM的眼光看函數調用

我們知道Java程式是不能單獨運行的,它需要運行在JVM上的,而JVM卻又需要跑在物理機上,所以它的任務很重,既要處理Java代碼,又要處理各種操作系統,硬體等問題。可以說瞭解了JVM,就瞭解了Java的全部,當然包括JNI。所以我們先以JVM的身份來看看Java代碼是怎樣跑起來的吧(只是粗略的內容,省去了很多步驟,為了突出我們在意的部分)。

運行Java代碼前,會先啟動一個JVM。在JVM啟動後,會載入一些必要的類,這些類中包含一個叫主類的類,也就是含有一個靜態成員函數,函數簽名為public static void main(String[] args)的方法。資源載入完成後,JVM就會調用主類的main方法,開始執行Java代碼。隨著代碼的執行,一個類依賴另一個類,層層依賴,共同完成了程式功能。這就是JVM的大概工作流程,可以說JVM就好比一座大橋,連接著Java大山和native大山。

現在問題來了,在Java程式中,某個類需要通過JNI技術訪問JVM以外的東西,那麼它需要怎樣告訴我(我現在是JVM)呢?需要一種方法 把普通的Java方法標記成特殊,這個標記就是native關鍵字(使用Kotlin時雖然也可以使用這個關鍵字,但是Kotlin有自己的關鍵字external)。當我執行到這個方法時,看到它不一樣的標記,我就會從其他地方而不是Class裡面尋找執行體,這就是一次JNI調用。也就是說對於Java程式來說,只需要將一個方法標記為native,在需要的地方調用這個方法,就可以完成JNI調用了。但是對於我,該怎樣處理這一次JNI調用呢?其實上面的尋找執行體的過程是一個跳轉問題,在C/C++的世界,跳轉問題就是指針問題。那麼這個指針它應該指向哪裡呢?

C/C++代碼是一個個函數(下文會將Java方法直接用方法簡稱,而C/C++函數直接用函數簡稱)組合起來的,每一個函數都是一個指針,這個特性恰好滿足我的需要。但是對於我,外面世界那麼大,我並不知道從哪裡,找什麼東西,給我的信息還是不夠。為了限定範圍,我規定,只有通過System.loadLibrary(“xxx”)載入的函數,我才會查找,其餘的我直接罷工(拋錯)。這一下子減輕了我的工作量,至少我知道從哪裡找了。

確定了範圍,下一步就是在這個範圍里確定真正的目標了。Java世界里怎樣唯一標識一個類呢,有的人會脫口而出——類名,其實不全對,因為類名可能會重名,我們需要全限定的類名,也就是包名加類名,如String的全限定類名就是java.lang.String。但是這和我們查找native的方法有什麼聯繫呢。當然有聯繫,既然一個全限定的類名是唯一的,那麼它的方法也是唯一的,那麼假如我規定以這個類的全限定類名加上方法名作為native函數的函數名,這樣我是不是就可以通過函數名的方式找到native的函數看呢,答案是肯定的,但是有瑕疵,因為Java系統支持方法重載,也就是一個類裡面,同名的方法可能有多個。那麼構成重載的條件是什麼呢,是參數列表不同。所以,結果就很顯然了,我在前面的基礎上再加上參數列表,組合成查找條件,我是不是就可以唯一確定某一個native函數了呢,這就是JNI的靜態註冊。

不過,既然我只需要確定指針的指向,那麼我能不能直接給指針賦值,而不是每次都去查找呢,雖然我不知道累,但是還是很耗費時間的。對於這種需求,我當然也是滿足的啦,你直接告訴我,我就不找了,我還樂意呢。而且,既然你都給我找到了,我就不需要下那麼多規定了,都放開,你說是我就相信你它是。這就是JNI的動態註冊。

JNI的函數註冊

上一節我們通過化身JVM的方式瞭解了JNI函數註冊的淵源,並且引出了兩種函數註冊方式。從例子上,我們也可以總結出兩種註冊方式的特點

註冊類型 優點 缺點
靜態註冊 JVM自動查找
實現簡單
函數名賊長,限制較多
查找耗時
動態註冊 運行快
對函數名無限制
實現複雜

那麼具體怎麼做呢?我們接著往下說。

靜態註冊

雖然靜態註冊限制比較多,但是都是一些淺顯的規則,更容易實施,所以先從靜態註冊開始講解。

靜態註冊有著明確的開發步驟

  1. 編寫Java類,聲明native方法;
  2. 使用java xxx.java將Java源文件編譯為class文件
  3. 使用javah xxx生成對應的.h文件
  4. 構建工具中引入.h文件
  5. 實現.h文件中的函數

上面的這個步驟是靜態開發的基本步驟,但是其實在如今強大的IDE面前,這些都不需要我們手動完成了,在Android Studio中,定義好native方法後,在方法上按alt + enter就可以生成正確的函數簽名,直接寫函數邏輯就可以了。但是學習一門學問,我們還是要抱著求真,求實的態度,所以我用一個例子來闡述一下這些規則,以加深讀者的理解。

Test.java

package me.hongui.demo

public class Test{
    native String jniString();
}

native-lib.cpp

#include <jni.h>

extern "C" jstring Java_me_hongui_demo_Test_jniString(JNIEnv *env, jobject thiz) {
    // TODO: implement jniString()
}

上面就是一個JNI函數在兩端聲明的例子,不難發現

  1. 函數簽名以Java_為首碼
  2. 首碼後面跟著類的全路徑,也就是包含包名和類名
  3. _作為路徑分隔符
  4. 函數的第一個參數永遠是JNIEnv *類型,第二個參數根據函數類型的不同而不同,static類型的方法,對應的是jclass類型,否則對應的是jobject類型。類型系統後面會詳細展開。

為什麼Java方法對應到C/C++函數後,會多兩個參數呢。我們知道JVM是多線程的,而我們的JNI方法可以在任何線程調用,那麼怎樣保證調用前後JVM能找到對應的線程呢,這就是函數第一個參數的作用,它是對線程環境的一種封裝,和線程一一對應,也就是說不能用一個線程的JNIEnv對象在另一個線程里使用。另外,它是一個C/C++訪問Java世界的視窗,JNI開發的絕大部分時間都是和JNIEnv打交道。

動態註冊

同樣按照開發過程,我們一步一步來完成。
我們把前面的Java_me_hongui_demo_Test_jniString函數名改成jniString(當然不改也可以,畢竟沒限制),參數列表保持不變,這時,我們就會發現Java文件報錯了,說本地方法未實現。其實我們是實現了的,只是JVM找不到。為了讓JVM能找到,我們需要向JVM註冊。
那麼怎麼註冊,在哪註冊呢,似乎哪裡都可以,又似乎都不可以。
前面說過,JVM只會查找通過System.loadLibrary(“xxx”); 載入的庫,所以要想使用native方法,首先要先載入包含該方法的庫文件,之後,才可使用。載入了庫,說明Java程式要開始使用本地方法了。在載入庫之後,調用方法之前,理論上都是可以註冊方法的,但是時機怎麼確定呢,JNI早就給我們安排好了。JVM在把庫載入進虛擬機後,會調用函數jint JNI_OnLoad(JavaVM *vm, void *reserved),以確認JNI的版本,版本信息會以返回值的形式傳遞給JVM,目前可選的值有JNI_VERSION_1_1,JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6。假如庫沒有定義這個函數,那麼預設返回的是JNI_VERSION_1_1,庫將會載入失敗,所以,為了支持最新的特性我們通常返回較高的版本。既然有了這麼好的註冊時機,那麼下一步就是實現註冊了。

但事情並沒有這麼簡單。由JNI_OnLoad函數參數列表可知,目前,可供使用的只有JVM,但是查閱JVM的API,我們並沒有發現註冊的函數——註冊函數是寫在JNIEnv類裡面的。恰巧的是,JVM提供了獲取JNIEnv對象的函數。

JVM有多個和JNIEnv相關的函數,在Android開發中,我們需要使用AttachCurrentThread來獲取JNIEnv對象,這個函數會返回執行狀態,當返回值等於JNI_OK的時候,說明獲取成功。有了JNIEnv對象,我們就可以註冊函數了。

先來看看註冊函數的聲明——jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods。返回值不用多說,和AttachCurrentThread一樣,指示執行狀態。難點在參數上,第一個參數是jclass類型,第二個是JNINativeMethod指針,都是沒見過的主。

為什麼需要這麼多參數呢,JVM不只需要一個函數指針嗎。還是唯一性的問題,記得前面的靜態註冊嗎,靜態註冊用全限定類型和方法,參數列表,返回值的組合確定了函數的唯一性。但是對於動態註冊,這些都是未知的,但是又是必須的。為了確定這些值,只能通過其他的方式。jclass就是限定方法的存在範圍,獲取jclass對象的方式也很簡單,使用JNIEnvjclass FindClass(const char* name)函數。參數需要串全限定符的類名,並且把.換成/,也就是類似me/hongui/demo/Test的形式,為啥這樣寫,後面會單獨拿一節出來細說。

第二個和第三個參數組合起來就是常見的數組參數形式。先來看看JNINativeMethod的定義。

typedef struct { 
    char *name; 
    char *signature; 
    void *fnPtr; 

} JNINativeMethod; 

有個編寫訣竅,按定義順序,相關性是從Java端轉到C/C++端,怎麼理解呢?name是只的Java端對應的native函數的名字,這是純Java那邊的事,Java那邊取啥名,這裡就是啥名。第二個signature代表函數簽名,簽名信息由參數列表和返回值組成,形如(I)Ljava/lang/String;,這個簽名就是和兩邊都有關係了。首先Java那邊的native方法定義了參數列表和返回值的類型,也就是限定了簽名的形式。其次Java的數據類型對應C/C++的轉換需要在這裡完成,也就是參數列表和返回值要寫成C/C++端的形式,這就是和C/C++相關了。最後一個fnPtr由名字也可得知它是一個函數指針,這個函數指針就是純C/C++的內容了,代表著Java端的native方法在C/C++對應的實現,也就是前文所說的跳轉指針的。知道了這些,其實我們還是寫不出代碼,因為,我們還有JNI的核心沒有說到,那就是類型系統。

JNI的類型系統

由於涉及到Java和C/C++兩個語言體系,JNI的類型系統很亂,但並非無跡可尋。首先需要明確的是,兩端都有自己的類型系統,Java里的booleanintString,C/C++的bool,int,string等等,遺憾的是,它們並不一一對應。也就是說C/C++不能識別Java的類型。既然類型不相容,談何調用呢。這也就是JNI欲處理的問題。

JNI類型映射

為瞭解決類型不相容的問題,JNI引入了自己的類型系統,類型系統里定義了和C/C++相容的類型,並且還對Java到C/C++的類型轉換關係做了規定。怎麼轉換的呢,這裡有個表

Java類型 C/C++類型 描述
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

乍一看,沒什麼特別的,不過就是加了j首碼(除了void),但是,這隻是基本類型,我們應該沒忘記Java是純面向對象的語言吧。各種複雜對象才是Java的主戰場啊。而對於複雜對象,情況就複雜起來了。我們知道在Java中,任何對象都是Object類的子類。那麼我們是否可以把除上面的基本類型以外的所有複雜類型都當作Object類的對象來處理呢,可是可以,但是不方便,像數組,字元串,異常等常用類,假如不做轉換使用起來比較繁瑣。為了方便我們開發,JNI又將複雜類型分為下麵這幾種情況

jobject                     (所有的Java對象)
    |
    |--jclass               (java.lang.Class)
    |--jstring              (java.lang.String)
    |--jarray               (數組)
    |     |
    |     |-- jobjectArray  (Object數組)
    |     |-- jbooleanArray (boolean數組)
    |     |-- jbyteArray    (byte數組)
    |     |-- jcharArray    (char數組)
    |     |-- jshortArray   (short數組)
    |     |-- jintArray     (int數組)
    |     |-- jlongArray    (long數組)
    |     |-- jfloatArray   (float數組)
    |     |-- jdoubleArray  (double數組)
    |--jthrowable           (java.lang.Throwable異常)

兩個表合起來就是Java端到C/C++的類型轉換關係了。也就是說,當我們在Java里聲明native代碼時,native函數參數和返回值的對應關係,也是C/C++調用Java代碼參數傳遞的對應關係。但是畢竟兩套系統還是割裂的,類型系統只定義了相容方式,並沒有定義轉換方式,雙方的參數還是不能相互識別,所以,JNI又搞了個類型簽名,欲處理類型的自動轉換問題。

JNI的類型簽名

類型簽名和類類型映射類似,也有對應關係,我們先來看個對應關係表

類型簽名 Java類型
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L fully-qualified-class ; fully-qualified-class
[type type[]
(arg-types)ret-type method type

對於基本類型,也很簡單,就是取了首字母,除了boolean(首字母被byte占用了),long(字母被用作了符合對象的首碼標識符)。
著重需要註意的是複合類型,也就是某個類的情況。它的簽名包含三部分,首碼L,中間是類型的全限定名稱,跟上尾碼;,三者缺一不可,並且限定符的分隔符要用/替換, 。 註意,類型簽名和類型系統不是一個概念。類型通常是純字元串的,用在函數註冊等地方,被JVM使用的。類型系統是和普通類型一樣的,可以定義變數,作為參數列表,被用戶使用的。 另外,數組對象也有自己的類型簽名,也是有著類型首碼[,後面跟著類型的簽名。最後的方法類型,也就是接下來我們著重要講的地方,它也是由三部分組成()和包含在()裡面的參數列表,()後面的返回值。這裡用到的所有類型,都是指類型簽名。

我們來看個例子

long f (int n, String s, boolean[] arr); 

它的類型簽名怎麼寫呢?我們來一步一步分析

  1. 確定它在Java裡面的類型,在表中找出對應關係,確定簽名形式。
  2. 用步驟1的方法確定它的組成部分的類型。
  3. 將確定好的簽名組合在一起

此例是方法類型,對應表中最後一項,所以簽名形式為(參數)返回值。該方法有三個參數,我們按照步驟1的方式逐一確定。

  1. int n對應int類型,簽名是I;
  2. String s對應String類型,是複合類型,對應表中倒數第三項,所以它的基本簽名形式是L全限定名;。而String的全限定名java.lang.String,用/替換,後變成java/lang/String。按步驟3,將它們組合在一起就是Ljava/lang/String;;
  3. boolean[] arr對應數組類型,簽名形式是[類型boolean的簽名是Z。組合在一起就是[Z;
  4. 最後來看返回值,返回值是long類型,簽名形式是J

按照簽名形式將這些信息組合起來就是(ILjava/lang/String;[Z)J註意類型簽名和簽名之間沒有任何分割符,也不需要,類型簽名是緊密排列的

再看動態註冊

有了JNI的類型系統的支持,回過頭來接著看動態註冊的例子,讓我們接著完善它。

  1. 用JVM對象獲取JNIEnv對象,即auto status=vm->AttachCurrentThread(&jniEnv, nullptr);
  2. 用步驟1獲取的JNIEnv對象獲取jclass對象,即auto cls=jniEnv->FindClass("me/hongui/demo/Test");
  3. 定義JNINativeMethod數組,即JNINativeMethod methods[]={{"jniString", "()Ljava/lang/String;",reinterpret_cast<void *>(jniString)}};,這裡的方法簽名可以參看上一節。
  4. 調用JNIEnvRegisterNatives函數。即status=jniEnv->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]));
  5. 當然,別忘了實現對應的native函數,即這裡的jniString——JNINativeMethod的第三個參數。

這五步就是動態註冊中JNI_OnLoad函數的實現模板了,主要的變動還是來自jclass的獲取參數和JNINativeMethod的簽名等,必須做到嚴格的一一對應。如下麵的例子

extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved){
    JNIEnv* jniEnv= nullptr;
    auto status=vm->AttachCurrentThread(&jniEnv, nullptr);
    if(JNI_OK==status){
        JNINativeMethod methods[]={{"jniString", "()Ljava/lang/String;",reinterpret_cast<void *>(jniString)}};
        auto cls=jniEnv->FindClass("me/hongui/demo/Test");
        status=jniEnv->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]));
        if(JNI_OK==status) {
            return JNI_VERSION_1_6;
        }
    }
    return JNI_VERSION_1_1;
}

在JNI中使用數據

前面磨磨唧唧說了這麼一大片,其實才講了一個問題——怎麼找到。雖然繁雜,但好在有跡可循,大不了運行奔潰。下麵要講的這個問題就棘手多了,需要一點點耐性和細心。這一部分也可以劃分成兩個小問題——訪問已知對象的數據,創建新對象。有一點還是要提一下,這裡的訪問還創建都是針對Java程式而言的,也就是說,對象是存在JVM虛擬機的堆上的,我們的操作都是基於堆對象的操作。 而在C/C++的代碼里,操作堆對象的唯一途徑就是通過JNIenv提供的方法。所以,這部分其實就是對JNIenv方法的應用講解。

Java對象的訪問

在面向對象的世界中,我們說訪問對象,通常指兩個方面的內容,訪問對象的屬性、調用對象的方法。這些操作在Java世界中,很好實現,但是在C/C++世界卻並非如此。在JNI的類型系統那一節,我們也瞭解到,Java中的複雜對象在C/C++中都對應著jobject這個類,顯然,無論Java世界中,那個對象如何牛逼,在C/C++中都是一視同仁的。為了實現C/C++訪問Java的複雜對象,結合訪問對象的方式,JNIEnv提供了兩大類方法,一類是對應屬性的,一類是對應方法的。藉助JNIEnv,C/C++就能實現訪問對象的目標了。而且它們還有一個較為統一的使用步驟:

  1. 根據要訪問的內容準備好對應id(fieldid或者methodid)。
  2. 確定訪問的對象和調用數據
  3. 通過JNIEnv的方法調用完成對象訪問

可以看出來,這使用步驟和普通面向對象的方式多了一些準備階段(步驟1,2)。之前提到過,這部分的內容需要的更多的是耐心和細心,不需要多少酷炫的操作,畢竟發揮空間也有限。這具體也體現在上面的步驟1,2。正是這個準備階段讓整個C/C++的代碼變得醜陋和脆弱,但是——又不是不能用,是吧。

看一個例子,Java里定義了一個Person類,類定義如下

public class Person(){
    private int age;
    private String name;

    public void setName(String name){
        return this.name=name;
    }
}

現在,我們在C/C++代碼里該怎麼訪問這個類的對象呢。假定需要讀取這個對象的age值,設置這個對象的name值。根據上面的步驟,我們有以下步驟

  1. 準備好agefieldid,setNamemethodid。根據JNIEnv的方法,我們可以看到四個相關的,fieldid,methodid各兩個,分普通的和靜態的。我們這裡都是普通的,所以確定的方法是GetFieldIDGetMethodID。第一個參數就是jclass對象,獲取方法前面已經說過,即通過JNIEnvFindClass方法,參數是全限定類名,以/替換.。後面兩個參數對應Java端的名稱和類型簽名,age屬於field,int的類型簽名是IsetName屬於method,簽名形式是(參數)返回值,這裡參數的簽名是Ljava/lang/String;,返回值的簽名是V,組合起來就是"(Ljava/lang/String;)V"
  2. 假定我們已經有了Person對象obj,通過Java傳過來的。
  3. 分別需要調用兩個方法,age是整形屬性,要獲取它的值,對應就需要使用GetIntField方法。setName是返回值為void的方法。所以應該使用CallVoidMethod

通過上面的分析,得出下麵的示例代碼。

auto cls=jniEnv->FindClass("me/hongui/demo/Person");
auto ageId=jniEnv->GetFieldID(cls,"age","I");
auto nameId=jniEnv->GetMethodID(cls,"setName","(Ljava/lang/String;)V");
jint age=jniEnv->GetIntField(obj,ageId);
auto name=jniEnv->NewStringUTF("張三");
jniEnv->CallVoidMethod(obj,nameId,name);

從上面的分析和示例來看,耐心和細心主要體現在

  1. 對要訪問的屬性或者方法要耐心確定類型和名稱,並且要保持三個步驟中的類型要一一對應。即調用GetFieldID的類型要以GetXXXField的類型保持一致,方法也是一樣。
  2. 對屬性或方法的靜態非靜態修飾也要留心,通常靜態的都需要使用帶有static關鍵字的方法,普通的則不需要。如GetStaticIntField就是對應獲取靜態整型屬性的值,而GetIntField則是獲取普通對象的整型屬性值。
  3. 屬性相關的設置方法都是類似於SetXField的形式,裡面的X代表著具體類型,和前面的類型系統中的類型一一對應,假如是複雜對象,則用Object表示,如SetObjectField。而訪問屬性只需要將首碼Set換成Get即可。對於靜態屬性,則是在SetX之間加上固定的Static,即SetStaticIntField這種形式。
  4. 方法調用則是以Call為首碼,後面跟著返回值的類型,形如CallXMethod的形式。這裡X代表返回值。如CallVoidMethod就表示調用對象的某個返回值為void類型的方法。同樣對應的靜態方法則是在CallX之間加上固定的Static,如CallStaticVoidMethod

向Java世界傳遞數據

向Java世界傳遞數據更需要耐心。因為我們需要不斷地構造對象,組合對象,設置屬性。而每一種都是上面Java對象的訪問的一種形式。

構造Java對象

C/C++構造Java對象和調用方法類似。但是,還是有很多值得關註的細節。根據前面的方法,我們構造對象,首先要知道構造方法的id,而得到id,我們需要得到jclass,構造方法的名字和簽名。我們知道在Java世界里,構造方法是和類同名的,但是在C/C++里並不是這樣,它有著特殊的名字——<init>,註意,這裡的<>不能少。也就是說無論這個類叫什麼,它的構造函數的名字都是<init> 而函數簽名的關鍵點在於返回值,構造方法的返回值都是void也就是對應簽名類型V

接前面那個Person類的例子,要怎樣構造一個Person對象呢。

  1. 通過JNIEnvFindClass得到就jclass對象。記得將'替換成/
  2. 根據需要得到合適的構造方法的id。我沒有定義構造方法,那麼編譯器會為它提供一個無參的構造方法。也就是函數簽名為()V。調用JNIEnvGetMethodID得到id。
  3. 調用JNIEnvNewObject創建對象,記得傳遞構造參數。我這裡不需要傳遞。

綜上分析,這個創建過程類似於如下示例

auto cls=env->FindClass("me/hongui/demo/Person");
auto construct=env->GetMethodID(cls,"<init>","()V");
auto age=env->GetFieldID(cls,"age","I");
auto name=env->GetFieldID(cls,"name","Ljava/lang/String;");
auto p=env->NewObject(cls,construct);
auto nameValue=env->NewStringUTF("張三");
env->SetIntField(p,age,18);
env->SetObjectField(p,name,nameValue);
return p

上面的示例有個有意思的點,其實示例中創建了兩個Java對象,一個是Person對象,另一個是String對象。因為在編程中,String出境的概率太大了,所以JNI提供了這個簡便方法。同樣特殊的還有數組對象的創建。並且因為數組類型不確定,還有多個版本的創建方法,如創建整型數組的方法是NewIntArray。方法簽名也很有規律,都是NewXArray的形式,其中X代表數組的類型,這些方法都需要一個參數,即數組大小。既然提到了數組,那麼數組的設置方法就不得不提。設置數組元素的值也有對應的方法,形如SetXArrayRegion,如SetIntArrayRegion就是設置整型數組元素的值。和Java世界不同的是,這些方法都是支持同時設置多個值的。整形數組的簽名是這樣——void SetIntArrayRegion(jintArray array,jsize start, jsize len,const jint* buf)第二個參數代表設置值的開始索引,第三個參數是數目,第四個參數是指向真正值的指針。其餘類型都是類似的。

讓數據訪問更進一步

有些時候,我們不是在調用native方法時訪問對象,而是在將來的某個時間。這在Java世界很好實現,總能找到合適的類存放這個調用時傳遞進來的對象引用,在後面使用時直接用就可以了。native世界也是這樣嗎?從使用流程上是一樣的,但是從實現方式上卻是很大不同。

Java世界是帶有GC的,也就是說,將某個臨時對象X傳遞給某個對象Y之後,X的生命周期被轉移到了Y上了,X不會在調用結束後被銷毀,而是在Y被回收的時候才會一同回收。這種方式在純Java的世界里沒有問題,但是當我們把這個臨時對象X傳遞給native世界,試圖讓它以Java世界那樣工作時,應用卻崩潰了,報錯JNI DETECTED ERROR IN APPLICATION: native code passing in reference to invalid stack indirect reference table or invalid reference: 0xxxxx。為什麼同樣的操作在Java裡面可以,在native卻不行呢。問題的根源就是Java的GC。GC可以通過各種垃圾檢測演算法判斷某個對象是否需要標記為垃圾。而在native世界,不存在GC,為了不造成記憶體泄漏,只能採取最嚴格的策略,預設調用native方法的地方就是使用Java對象的地方。所以在native方法調用的作用域結束後,臨時對象就被GC標記為垃圾,後面想再使用,可能已經被回收了。還好,強大的JNIEnv類同樣提供了方法讓我們改變這種預設策略——NewGlobalRef。對象只需要通過這種方式告訴JVM,它想活得更久一點,JVM在執行垃圾檢測的時候就不會把它標記為垃圾,這個對象就會一直存。在,直到調用DeleteGlobalRef這裡NewGlobalRefDeleteGlobalRef是一一對應的,而且最好是再不需要對象的時候就調用DeleteGlobalRef釋放記憶體,避免記憶體泄漏。

總結

JNI開發會涉及到Java和C/C++開發的知識,在用C/C++實現JNI時,基本思想就是用C/C++語法寫出Java的邏輯,也就是一切為Java服務。JNI開發過程中,主要要處理兩個問題,函數註冊和數據訪問。

函數註冊推薦使用動態註冊,在JNI_OnLoad函數中使用JNIEnvRegisterNatives註冊函數,註意保持Java的native方法和類型簽名的一致性,複合類型不要忘記首碼L、尾碼;,並將.替換為/

數據訪問首先需要確定訪問周期,需要在多個地方或者不同時間段訪問的對象,記得使用NewGlobalRef阻止對象被回收,當然還要記得DeleteGlobalRef。訪問對象需要先拿到相應的id,然後根據訪問類型確定訪問方法。設置屬性通常是SetXField的形式,獲取屬性值通常是GetXField的形式。調用方法,需要根據返回值的類型確定調用方法,通常是CallXMethod的形式。當然,這些都是針對普通對象的,假如需要訪問靜態屬性或者方法,則是在普通版本的X前面加上Static。這裡的所有X都是指代類型,除了基本類型外,其他對象都用Object替換。

在註冊函數和訪問數據的時候需要時刻關註的就是數據類型。C/C++數據類型除了基本類型外都不能直接傳遞到Java里,需要通過創建對象的方式傳遞。一般的創建對象方式NewObject可以創建任何對象,而對於使用頻繁的字元串和數組有對應的快速方法NewStringUTFNewXArray。向Java傳遞字元串和數組,這兩個方法少不了。

青山不改,綠水長流,咱們下期見!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ### 前言 在項目初創階段,經常會遇到各種文件操作,拷貝頭文件,庫,批量重命名等。文件結構一複雜,這就將是個無聊的工作。 ### 查找文件 `find`可以在目錄結構中搜索文件,這是它在`man`裡面的作用描述。那麼怎麼搜索呢?有多種方式,按文件時間,大小,按文件名,路徑名,按文件類型,許可權,按用 ...
  • > 本地安裝的 nginx 比較好維護,配置起來也方便,比 yum 的安裝方式要更好的運維和使用,此篇技術貼親測可用,實測了使用 nginx 代理 nacos 的伺服器集群。 ## 一、安裝各種依賴 gcc安裝,nginx源碼編譯需要 ```bash yum install gcc-c++ #PCR ...
  • 關機命令、重啟命令,創建用戶、刪除用戶、修改密碼、切換用戶、切換到超級用戶、禁用/解鎖用戶賬戶、修改信息、組管理、列出用戶、修改用戶屬性、用戶許可權管理、用戶信息管理、用戶登錄信息、系統管理員操作,瀏覽和切換目錄、創建和刪除目錄、複製、移動和重命名目錄、查找和搜索目錄、查看目錄信息、修改目錄許可權、查看... ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家講的是**幾家主流QuadSPI NOR Flash廠商關於QE位與IO功能復用關聯設計**。 痞子衡之前寫過一篇文章 [《串列NOR Flash下載/啟動常見影響因素之QE bit》](https://www.cnblogs.com/henj ...
  • ## 一、問題一:不能正常開啟虛擬機 創建虛擬機後,我錯誤的使用了`shutdown now`的關機命令,每次關機不能正常啟動虛擬機,需要重啟VMware的五大服務,然後重啟電腦才能正常啟動虛擬機。 ## 二、問題二:啟動虛擬機黑屏的解決方案 偶爾啟動虛擬機時,會一直長時間的黑屏沒有反應,從網上查找 ...
  • 本文主要驗證 Elasticsearch 快照在 [Easysearch](http://www.infinilabs.com/docs/latest/easysearch/overview) 中進行數據恢復。 ## 準備測試數據 ### 索引 ![](https://www.infinilabs. ...
  • 在Android開發中,有時候出於安全,性能,代碼共用的考慮,需要使用C/C++編寫的庫。雖然在現代化工具鏈的支持下,這個工作的難度已經大大降低,但是畢竟萬事開頭難,初學者往往還是會遇到很多不可預測的問題。本篇就是基於此背景下寫的一份簡陋指南,希望能對剛開始編寫C/C++庫的讀者有所幫助。同時為了盡 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...