什么是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=https://www.cnblogs.com/honguilee/archive/2023/06/17/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傳遞字串和陣列,這兩個方法少不了,
青山不改,綠水長流,咱們下期見!
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/555485.html
標籤:其他
下一篇:返回列表