Android中用JNI實現與原生代碼通信

好久沒搞NDK的東西了,第一次學習NDK還是14年的時候,最近又需要使用,所以複習一下。本篇筆記主要記錄使用Java原生接口技術實現Java應用程序和原生代碼之間通信


JNI是Java程序設計語言功能最強的特徵,它允許Java類的某些方法原生實現,同時讓它們能夠像普通Java方法一樣被調用和使用。這些原生方法也可以使用Java對象,使用方法與Java代碼使用Java對象的方法相同。原生方法可以創建新的Java對象或者使用Java應用程序創建的對象,這些Java應用程序可以檢查、修改和調用這些對象的方法以執行任務。


1.Java代碼如何調用原生方法

2.聲明原生方法

3.在共享庫中載入原生模塊

4.在C/C++中實現原生方法


loadLibrary的參數也不包含共享庫的位置。Java庫路徑,也就是系統屬性java.library.path保存loadLibirary方法在共享庫搜索的目錄列表,Android上的Java庫路徑包含/vendor/lib和/system/lib。


需要強調的是,loadLibrary在掃描Java庫路徑時,一旦發現同名的庫,立即加載共享庫。因爲Java庫路徑的第一組目錄是Android系統目錄,

爲了避免與系統庫命名衝突,強烈建議Android開發人員爲每個共享庫選擇唯一的名字。



C源代碼文件以jni.h頭文件包含語句開關,這個頭文件中包含JNI數據類型和函數的定義。


方法聲明

JNIEXPORT jstring


JNICALL
Java_com_generalandroid_algorithmwithndk_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

第一個參數JNIEnv是指向可用JNI函數表的接口指針;第二個參數jobject是Java對象引用。


JNIEnv接口指針

原生代碼通過JNIEnv接口指針提供的各種函數來使用虛擬機的功能。JNIEnv是一個指向線程-局部數據的指針,而線程-局部數據中包含指向

函數表的指針。實現原生方法的函數將JNIEnv接口指針作爲它們的第一個參數。(注意:傳遞給每一個原生方法調用的JNIEnv接口指針在與方法調用相關的線程中也有效,但是它不能被緩存以及被其他線程使用。)



原生代碼是C與原生代碼是C++其調用JNI函數的語法不同。C代碼中,JNIEnv是指向JNINativeInterface結構的指針,爲了訪問任何一個JNI函數,該指針需要首先被解引用。因爲C代碼中的JNI函數不瞭解當前的JNI環境,JNIEnv實例應該作爲第一個參數傳遞給每一個JNI函數調用調用者,調用格式如下:

return (*env)->NewStringUTF(env,"Hello from JNI!");


在C++代碼中,JNIEnv實際上是C++類實例,JNI函數以成員函數的形式存在。因爲JNI方法已經訪問了當前的JNI環境,因此JNI方法調用不要求JNIEnv實例作參數。

return env->NewStringUTF("Hello from JNI");


 (2)實例方法與靜態方法

Java程序設計語言有兩類方法:實例方法和靜態方法。實例方法與類實例相關,它們只能在類實例中調用。靜態方法不與類實例相關,它們可以在靜態上下文直接調用。靜態方法和實例方法均可以聲明爲原生的,可以通過JNI技術以原生代碼的形式提供它們的實現。原生實例方法通過第二個參數獲取實例引用,該參數是jobject類型的。原生靜態方法通過第二個參數獲取類引用,該參數是jclass類型的。


JNI提供了自己的數據類型從而讓原生代碼瞭解Java數據類型。

Java中有兩種數據類型:

1.基本數據類型:布爾型、字節型、字符型、短整型、整型、長整型、浮點型和雙精度型。

2.引用類型:字符串類、數組類及其他類。


Java基本數據類型

Java類型JNI類型C/C++類型大小
BooleanJbooleanunsigned_char無符號8位
ByteJbytechar有符號8位
CharJcharunsigned_short無符號16位
ShortJshortshort有符號16位
IntJintint有符號32位
LongJlonglong_long有符號64位
FloatJfloatfloat32位
DoubleJdoubledouble64位



Java引用類型

Java類型原生類型
java.lang.Classjclass
java.lang.Thowablejthrowable
java.lang.Stringjstring
Other objectsjobjects
java.lang.Object[]jobjectArray
boolean[]jbooleanArray
byte[]jbyteArray
char[]jcharArray
short[]jshortArray
int[]jintArray
long[]jlongArray
float[]jfloatArray
double[]jdoubleArray
Other arraysJarray


引用類型以不透明的引用方式傳遞給原生代碼,而不是以原生數據類型的形式呈現,因此引用類型不能直接使用和修改。JNI提供了與這些引用類型密切相關的一組API,這些API通過JNIEnv接口指針提供給原生函數。
  • 字符串
  • 數組
  • NIO緩衝區
  • 字段
  • 方法
 
字符串操作
JNI把Java字符串當成引用類型來處理。這些引用類型並不像原生C字符串一樣可以直接使用,JNI提供了Java字符串與C字符串之間相互轉換的必要函數。因爲Java字符串對象是不可變的,因此JNI不提供任何修改現有的Java字符串內容的函數。
JNI支持Unicode編碼格式和UTF-8編碼格式的字符串,還提供兩組函數通過JNIEnv接口指針處理這些字符串編碼。

創建字符串

可以在原生代碼中用NewString函數構建Unicode編碼格式的字符串實例,用NewStringUTF函數構建UTF-8編碼格式的字符串實例。
在內存溢出的情況下,這些函數返回NULL以通知原生代碼虛擬機中拋出異常,這樣原生代碼就會停止運行。

把Java字符串轉換成C字符串

爲了在原生代碼中使用Java字符串,需要先將Java字符串轉換成C字符串。用GetStringChars函數可以將Unicode格式的Java字符串
轉換成C字符串,用GetStringUTFChars函數可以將UTF-8格式的Java字符串轉換成C字符串。這些函數的第三個參數均爲可選參數,該
可選參數名是isCopy,它讓調用者確定返回的C字符串地址指向副本還是指向堆中的固定對象。

釋放字符串
通過JNI GetStringChars 函數和GetStringUTFChars函數獲得的C字符串在原生代碼中使用完之後需要正確地釋放,否則將會引起內部泄露。
JNI提供了ReleaseStringChars函數釋放Unicode編碼格式的字符串,而用ReleaseStringUTFChars函數釋放UTF-8編碼格式的字符串。


數組操作
JNI把Java數組當成引用類型來處理,JNI提供必要的函數訪問和處理Java數組。

1.創建數組
用New<Type>Array函數在原生代碼中創建數組實例,其中<Type>可以是Int、Char和Boolean等,例如NewIntArray。
與NewString函數一樣,在內存溢出的情況下,New<Type>Array函數將返回NULL以通知原生代碼虛擬機中有異常拋出,
這樣原生代碼就會停止運行。

訪問數組元素
JNI提供兩種訪問Java數組元素的方法,可以將數組的代碼複製成C數組或者讓JNI提供直接指向數組元素的指針。

對副本的操作
Get<Type>ArrayRegion函數將給定的基本Java數組複製到給定的C數組中,如下:
jint   nativeArray[10];
(*env)->GetIntArrayRegion(env,javaArray,0,10,nativeArray);
原生代碼可以像使用普通的C數組一樣使用和修改數組元素。當原生代碼想將所做的修改提交給Java數組時,可以使用Set<Type>ArrayRegion函數將C數組複製回Java數組中。如下:
(*env)->SetIntArrayRegion(env,javaArray,0,10,nativeArray);

當數組很大時,爲了對數組進行操作而複製數組會引起性能問題。在這種情況下,如果可能的話,原生代碼應該只獲取或設置數組元素區域而不是獲取整個數組。另外,JNI提供了不同的函數集以獲得數組元素而非其副本的直接指針。

對直接指針的操作
可能的話,原生代碼可以用Get<Type>ArrayElements函數獲取指向數組元素的直接指針。函數帶有三個參數,第三個參數是可選參數,該可選參數名是isCopy,讓調用者確定返回的C字符串地址指向副本還是指向堆中的固定對象。
如下:
jint *   nativeDirectArray;
jboolean isCopy;
nativeDirectArray=(*env)->GetIntArrayElements(env,javaArray,&isCopy);
因爲可以像普通的C數組一樣訪問和處理數組元素,因此JNI沒提供訪問和處理數組元素的方法,JNI要求原生代碼用完這些指針立即釋放,否則
會出現內存溢出。原生代碼可以使用JNI提供的Release<Type>ArrayElements函數釋放Get<Type>ArrayElements函數返回的C數組。
(*env)->ReleaseIntArrayElements(env,javaArray,nativeDirectArray,0);
該函數帶有四個參數,第四個參數是釋放模式,如下:

釋放模式動作
0將內容複製回來並釋放原生數組
JNI_COMMIT將內容複製回來但是不釋放原生數組,一般用於週期性地更新一個Java數組
JNI_ABORT釋放原生數組但不用將內容複製回來

NIO操作
原生I/O(NIO)在緩衝管理區、大規模網絡和文件I/O及字符集支持方面的性能有所改進。JNI提供了在原生代碼中使用NIO的函數 。與數組操作相比,NIO緩衝區的數據傳送性能較好,更適合在原生代碼和Java應用程序之間傳送大量數據
1.創建直接字節緩衝區
原生代碼可以創建Java應用程序使用的直接字節緩衝區,該過程是以提供一個原生C字節數組爲基礎。
如下:
unsigned char * buffer=(unsigned char *)malloc(1024);
....
jobject directBuffer;
directBuffer=(*env)->NewDirectByteBuffer(env,buffer,1024);

注意:原生方法中的內存分配超出了虛擬機的管理範圍,且不能用虛擬機的垃圾回收器回收原生方法中的內存。原生函數應該
通過釋放未使用的內存分配以避免內存泄露來正確管理內存。


2.直接字節緩衝區獲取
Java應用程序中也可以創建直接字節緩衝區,在原生代碼中調用GetDirectBufferAddress函數可以獲得原生字節數組的內存地址。如下:

unsigned char * buffer;
buffer=(unsigned char *)(*env)->GetDirectBufferAddress(env,directBuffer);


訪問域

Java有兩類域:實例域和靜態域。類的每個實例都有自己的實例域副本,而一個類的所有實例共享同一個靜態域。
JNI提供了訪問兩類域的函數。

獲取域ID
JNI提供了用域ID訪問兩類域的方法,可以通過給定實例的class對象獲取域ID,用GetObjectClass函數可以獲得class對象。
jclass clazz;
clazz=(*env)->GetOjectClass(env,instance);
有兩個獲得域ID的函數分別適用於不同類型域,GetFieldId函數用於獲取實例域,GetStaticFieldId用於獲取靜態域ID。這兩個函數均
返回jfieldID類型的域ID。

jfieldID instanceFieldId;
instanceFieldId=(*env)->GetFieldID(env,clazz,"instanceField","Ljava/lang/String;");

jfieldID staticFieldId;
staticFieldId=(*env)->GetStaticFieldID(env,clazz,"staticField","Ljava/lang/String");

兩個函數的最後一個參數是Java中表示域類型的域描述符。 
注意:爲了提高應用程序的性能,可以緩存域ID。一般總是緩存使用最頻繁的域ID。

獲取域
在獲得域ID之後,可以用Get<Type>Field函數獲得實際的實例域,用GetStatic<Type>Field 函數獲得靜態域。
jstring instanceField;
instanceField=(*env)->GetObjectField(env,instance,instanceFieldId);
jstring staticField;
staticField=(*env)->GetStaticObjectField(env,clazz,staticFieldId);
在內存溢出的情況下,這些函數均返回NULL,此時原生代碼不會繼續執行。
注意:
獲得單個閾值需要調用兩到三個JNI函數,原生代碼回到Java中獲取每個單獨的域值,這給應用程序增加了額外的負擔,進而導致性能下降。
強烈建議將所有需要的參數傳遞給原生方法調用,而不是讓原生代碼回到Java中。


調用方法
與域一樣,Java中有兩類方法:實例方法和靜態方法。JNI提供訪問兩類方法的函數。

獲取方法ID

JNI提供了用方法ID訪問兩類方法的途徑,可以用給定實例的class對象獲得方法ID。用GetMethodID函數獲得實例方法的方法ID。
用GetStaticMethodID函數獲得靜態域的方法ID。兩個函數均返回jmethodID類型的方法ID。
jmethodID instanceMethodId;
instanceMethodId=(*env)->GetMethodID(env,clazz,"instanceMethod","()Ljava/lang/String");

jmethodID staticMethodId;
staticMethodId=(*env)->GetStaticMethodID(env,clazz,"staticMethod","()Ljava/lang/String")

與字段ID獲取方法一樣,兩個函數的最後一個參數均表示方法描述符,在Java中它表示方法簽名。

注意:爲了提升應用程序的性能,可以緩存方法ID。一般總是緩存使用最頻繁的方法ID。

調用方法

可以以方法ID爲參數通過Call<Type>Method類函數調用實際的實例方法,用CallStatic<Type>Field類函數調用靜態方法。
jstring instanceMethodResult;
instanceMethodResult=(*env)->CallStringMethod(env,instance,instanceMethodId);
jstring staticMethodResult;
staticMethodResult=(*env)->CallStaticStringMethod(env,clazz,staticMethodId);
在內存溢出的情況下,這些函數均返回NULL,此時原生代碼不會繼續執行。

注意:Java和原生代碼之間的轉換是代價較大的操作,強烈建議規劃Java代碼和原生代碼的任務時考慮這種代價,最小化這種轉
換可以大大提高應用程序的性能。

域和方法描述符

Java類型簽名映射
Java類型簽名
BooleanZ
ByteB
CharC
ShortS
IntI
LongJ
FloatF
DoubleD
fully-qualified-classLfully-qualified-class
type[][type
method type(arg-type)ret-type

原生代碼不易產生異常,處理域和調用Java方法可能導致Java異常。

異常處理

異常處理是Java程序設計語言的重要功能,JNI中的異常行爲與Java中的有所不同,在Java中,當拋出一個異常時,虛擬機停止執行代碼塊
並進入調用棧反向檢查能處理特定類型異常的異常處理程序代碼塊,這也叫做捕獲異常。虛擬機清除異常並將控制權交給異常處理程序。相比之下,JNI要求開發人員在異常發生後顯示地實現異常處理流。


捕獲異常
JNIEnv接口提供了一組與異常相關的函數集,在運行過程中可以使用Java類查看這些函數。JNI提供了ExceptionOccurred函數查詢虛擬機中是否有掛起的異常。在使用完之後,異常處理程序需要用ExceptionClear函數顯式地清除異常。

jthrowable ex;
....
(*env)->CallVoidMethod(env,instance,throwingMethodId);
ex=(*env)->ExceptionOccurred(env);
if(0!=ex){
(*env)->ExceptionClear(env);
}

拋出異常

JNI也允許原生代碼拋出異常。因爲異常是Java類,應該先用FindClass函數找到異常類,用ThrowNew函數可以初始化且拋出新的異常。

jclass clazz;
....
clazz =(*env)->FindClass(env,"java/lang/NullPointerException");
if(0!=clazz){
(*env)->ThrowNew(env,clazz,"Exception message");
}

因爲原生函數的代碼執行不受虛擬機的控制,因此拋出異常並不會停止原生函數的執行並把控制權轉交給異常處理程序。到拋出異常時,原生函數應該釋放所有已分配的原生資源,例如內存及合適的返回值等。通過JNIEnv接口獲得的引用是局部引用且一旦返回原生函數,它們自動地被虛擬機釋放。


局部和全局引用

引用在Java程序設計中扮演非常重要的角色。虛擬機通過追蹤類實例的引用並收回不再引用的垃圾來管理類實例的使用期限。因爲原生代碼不是一個管理環境,因此JNI提供了一組函數允許原生代碼顯式地管理對象引用及使用期間原生代碼。JNI支持三種引用:局部引用、全局引用和弱全局引用。

局部引用

大多數JNI函數返回局部引用。局部引用不能在後續的調用中被緩存及重用,主要因爲它們的使用期限僅限於原生方法,一旦原生函數返回,局部引用即被釋放。


全局引用

全局引用在原生方法的後續調用過程中依然有效,除非它們被原生代碼顯示釋放。

創建全局引用

可以用NewGlobalRef函數將局部引用初始化爲全局引用,如:
jclass localClazz;
jclass globalClazz;
....
localClazz=(*env)->FindClass(env,"java/lang/String");
globalClazz=(*env)->NewGlobalRef(env,localClazz);
....
(*env)->DeleteLocalRef(env,localClazz);

刪除全局引用

當原生代碼不再需要一個全局引用時,可以隨時用DeleteGlobalRef函數釋放它。如:
(*env)->DeleteGlobalRef(env,globalClazz);


弱全局引用

全局引用的另一種類型是弱全局引用。與全局引用一樣,弱全局引用在原生方法的後續調用過程中依然有效。與全局引用不同,弱全局
引用並不阻止潛在的對象被垃圾收回。

創建弱全局引用

 可以用NewWeakGlobalRef函數對弱全局引用進行初始化。
jclass weakGlobalClazz;
weakGlobalClazz=(*env)->NewWeakGlobalRef(env,localClazz);

弱全局引用的有效性檢驗

可以用IsSameObject函數檢驗一個弱全局引用是否仍然指向活動的類實例,如下:
if(JNI_FALSE==(*env)->IsSameObject(env,weakGlobalClazz,NULL)){
//對象仍然處於活動狀態且可以使用
}else{
//對象被垃圾回收器收回,不能使用
}

刪除弱全局引用

可以隨時用DeleteWeakGlobalRef函數釋放弱全局引用,如下:
(*env)->DeleteWeakGlobalRef(env,weakGlobalClazz);

全局引用顯示釋放前一直有效,它們可以被其他原生函數及原生線程使用。


備註說明:文中內容摘自Android NDK C++高級編程這本書。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章