### 什麼是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自動查找 實現簡單 |
函數名賊長,限制較多 查找耗時 |
動態註冊 | 運行快 對函數名無限制 |
實現複雜 |
那麼具體怎麼做呢?我們接著往下說。
靜態註冊
雖然靜態註冊限制比較多,但是都是一些淺顯的規則,更容易實施,所以先從靜態註冊開始講解。
靜態註冊有著明確的開發步驟
- 編寫Java類,聲明
native
方法; - 使用
java xxx.java
將Java源文件編譯為class文件 - 使用
javah xxx
生成對應的.h
文件 - 構建工具中引入
.h
文件 - 實現
.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函數在兩端聲明的例子,不難發現
- 函數簽名以
Java_
為首碼 - 首碼後面跟著類的全路徑,也就是包含包名和類名
- 以
_
作為路徑分隔符 - 函數的第一個參數永遠是
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
對象的方式也很簡單,使用JNIEnv
的jclass 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里的boolean
,int
,String
,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);
它的類型簽名怎麼寫呢?我們來一步一步分析
- 確定它在Java裡面的類型,在表中找出對應關係,確定簽名形式。
- 用步驟1的方法確定它的組成部分的類型。
- 將確定好的簽名組合在一起
此例是方法類型,對應表中最後一項,所以簽名形式為(參數)返回值
。該方法有三個參數,我們按照步驟1的方式逐一確定。
int n
對應int
類型,簽名是I
;String s
對應String
類型,是複合類型,對應表中倒數第三項,所以它的基本簽名形式是L全限定名;
。而String
的全限定名java.lang.String
,用/
替換,
後變成java/lang/String
。按步驟3,將它們組合在一起就是Ljava/lang/String;
;boolean[] arr
對應數組類型,簽名形式是[類型
,boolean
的簽名是Z
。組合在一起就是[Z
;- 最後來看返回值,返回值是
long
類型,簽名形式是J
。
按照簽名形式將這些信息組合起來就是(ILjava/lang/String;[Z)J
,註意類型簽名和簽名之間沒有任何分割符,也不需要,類型簽名是緊密排列的。
再看動態註冊
有了JNI的類型系統的支持,回過頭來接著看動態註冊的例子,讓我們接著完善它。
- 用JVM對象獲取
JNIEnv
對象,即auto status=vm->AttachCurrentThread(&jniEnv, nullptr);
- 用步驟1獲取的
JNIEnv
對象獲取jclass
對象,即auto cls=jniEnv->FindClass("me/hongui/demo/Test");
- 定義
JNINativeMethod
數組,即JNINativeMethod methods[]={{"jniString", "()Ljava/lang/String;",reinterpret_cast<void *>(jniString)}};
,這裡的方法簽名可以參看上一節。 - 調用
JNIEnv
的RegisterNatives
函數。即status=jniEnv->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]));
。 - 當然,別忘了實現對應的
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++就能實現訪問對象的目標了。而且它們還有一個較為統一的使用步驟:
- 根據要訪問的內容準備好對應id(fieldid或者methodid)。
- 確定訪問的對象和調用數據
- 通過
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
值。根據上面的步驟,我們有以下步驟
- 準備好
age
的fieldid
,setName
的methodid
。根據JNIEnv
的方法,我們可以看到四個相關的,fieldid
,methodid
各兩個,分普通的和靜態的。我們這裡都是普通的,所以確定的方法是GetFieldID
和GetMethodID
。第一個參數就是jclass
對象,獲取方法前面已經說過,即通過JNIEnv
的FindClass
方法,參數是全限定類名,以/
替換.
。後面兩個參數對應Java端的名稱和類型簽名,age
屬於field,int
的類型簽名是I
,setName
屬於method,簽名形式是(參數)返回值
,這裡參數的簽名是Ljava/lang/String;
,返回值的簽名是V
,組合起來就是"(Ljava/lang/String;)V"
。 - 假定我們已經有了
Person
對象obj
,通過Java傳過來的。 - 分別需要調用兩個方法,
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);
從上面的分析和示例來看,耐心和細心主要體現在
- 對要訪問的屬性或者方法要耐心確定類型和名稱,並且要保持三個步驟中的類型要一一對應。即調用
GetFieldID
的類型要以GetXXXField
的類型保持一致,方法也是一樣。 - 對屬性或方法的靜態非靜態修飾也要留心,通常靜態的都需要使用帶有
static
關鍵字的方法,普通的則不需要。如GetStaticIntField
就是對應獲取靜態整型屬性的值,而GetIntField
則是獲取普通對象的整型屬性值。 - 屬性相關的設置方法都是類似於
SetXField
的形式,裡面的X
代表著具體類型,和前面的類型系統中的類型一一對應,假如是複雜對象,則用Object
表示,如SetObjectField
。而訪問屬性只需要將首碼Set
換成Get
即可。對於靜態屬性,則是在Set
和X
之間加上固定的Static
,即SetStaticIntField
這種形式。 - 方法調用則是以
Call
為首碼,後面跟著返回值的類型,形如CallXMethod
的形式。這裡X
代表返回值。如CallVoidMethod
就表示調用對象的某個返回值為void
類型的方法。同樣對應的靜態方法則是在Call
和X
之間加上固定的Static
,如CallStaticVoidMethod
。
向Java世界傳遞數據
向Java世界傳遞數據更需要耐心。因為我們需要不斷地構造對象,組合對象,設置屬性。而每一種都是上面Java對象的訪問的一種形式。
構造Java對象
C/C++構造Java對象和調用方法類似。但是,還是有很多值得關註的細節。根據前面的方法,我們構造對象,首先要知道構造方法的id,而得到id,我們需要得到jclass
,構造方法的名字和簽名。我們知道在Java世界里,構造方法是和類同名的,但是在C/C++里並不是這樣,它有著特殊的名字——<init>
,註意,這裡的<>
不能少。也就是說無論這個類叫什麼,它的構造函數的名字都是<init>
。 而函數簽名的關鍵點在於返回值,構造方法的返回值都是void
也就是對應簽名類型V
。
接前面那個Person
類的例子,要怎樣構造一個Person
對象呢。
- 通過
JNIEnv
的FindClass
得到就jclass
對象。記得將'
替換成/
。 - 根據需要得到合適的構造方法的id。我沒有定義構造方法,那麼編譯器會為它提供一個無參的構造方法。也就是函數簽名為
()V
。調用JNIEnv
的GetMethodID
得到id。 - 調用
JNIEnv
的NewObject
創建對象,記得傳遞構造參數。我這裡不需要傳遞。
綜上分析,這個創建過程類似於如下示例
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
。這裡NewGlobalRef
,DeleteGlobalRef
是一一對應的,而且最好是再不需要對象的時候就調用DeleteGlobalRef
釋放記憶體,避免記憶體泄漏。
總結
JNI開發會涉及到Java和C/C++開發的知識,在用C/C++實現JNI時,基本思想就是用C/C++語法寫出Java的邏輯,也就是一切為Java服務。JNI開發過程中,主要要處理兩個問題,函數註冊和數據訪問。
函數註冊推薦使用動態註冊,在JNI_OnLoad
函數中使用JNIEnv
的RegisterNatives
註冊函數,註意保持Java的native
方法和類型簽名的一致性,複合類型不要忘記首碼L
、尾碼;
,並將.
替換為/
。
數據訪問首先需要確定訪問周期,需要在多個地方或者不同時間段訪問的對象,記得使用NewGlobalRef
阻止對象被回收,當然還要記得DeleteGlobalRef
。訪問對象需要先拿到相應的id,然後根據訪問類型確定訪問方法。設置屬性通常是SetXField
的形式,獲取屬性值通常是GetXField
的形式。調用方法,需要根據返回值的類型確定調用方法,通常是CallXMethod
的形式。當然,這些都是針對普通對象的,假如需要訪問靜態屬性或者方法,則是在普通版本的X
前面加上Static
。這裡的所有X
都是指代類型,除了基本類型外,其他對象都用Object
替換。
在註冊函數和訪問數據的時候需要時刻關註的就是數據類型。C/C++數據類型除了基本類型外都不能直接傳遞到Java里,需要通過創建對象的方式傳遞。一般的創建對象方式NewObject
可以創建任何對象,而對於使用頻繁的字元串和數組有對應的快速方法NewStringUTF
,NewXArray
。向Java傳遞字元串和數組,這兩個方法少不了。
青山不改,綠水長流,咱們下期見!