JNI與NDK的關系
NDK可以為我們生成了C/C++的動態(tài)鏈接庫骡技,JNI是java和C/C++溝通的接口,兩者與android沒有半毛錢關系囤萤,只因為安卓是java程序語言開發(fā),然后通過JNI又能與C/C++溝通涛舍,所以我們可以使用NDK+JNI來實現“Java+C”的開發(fā)方式唆途。
JNIEnv與JavaVM
JNIEnv 概念 : 是一個線程相關的結構體, 該結構體代表了 Java 在本線程的運行環(huán)境 ;
JNIEnv 與 JavaVM : 注意區(qū)分這兩個概念;
– JavaVM : JavaVM 是 Java虛擬機在 JNI 層的代表, JNI 全局只有一個;
– JNIEnv : JavaVM 在線程中的代表, 每個線程都有一個, JNI 中可能有很多個 JNIEnv;
JNIEnv 作用 :
– 調用 Java 函數 : JNIEnv 代表 Java 運行環(huán)境, 可以使用 JNIEnv 調用 Java 中的代碼;
– 操作 Java 對象 : Java 對象傳入 JNI 層就是 Jobject 對象, 需要使用 JNIEnv 來操作這個 Java 對象;
JNIEnv 體系結構
線程相關 : JNIEnv 是線程相關的, 即 在 每個線程中 都有一個 JNIEnv 指針, 每個JNIEnv 都是線程專有的, 其它線程不能使用本線程中的 JNIEnv, 線程 A 不能調用 線程 B 的 JNIEnv;
*.so的入口函數
JNI_OnLoad()與JNI_OnUnload()
當Android的VM(Virtual Machine)執(zhí)行到System.loadLibrary()函數時,首先會去執(zhí)行C組件里的JNI_OnLoad()函數肛搬。它的用途有二:
(1)告訴VM此C組件使用那一個JNI版本滚婉。如果你的.so檔沒有提供JNI_OnLoad()函數帅刀,VM會默認該.so檔是使用最老的JNI 1.1版本。由于新版的JNI做了許多擴充扣溺,如果需要使用JNI的新版功能,例如JNI 1.4的java.nio.ByteBuffer,就必須藉由JNI_OnLoad()函數來告知VM锥余。
(2)由于VM執(zhí)行到System.loadLibrary()函數時,就會立即先呼叫JNI_OnLoad()嘲恍,所以C組件的開發(fā)者可以藉由JNI_OnLoad()來進行C組件內的初期值之設定(Initialization) 雄驹。
JNI字符串函數
常用的JNI函數將在后續(xù)介紹医舆,這里給出其中的字符串操作函數的函數名以及相關描述俘侠。
GetStringChars
ReleaseStringChars 獲得/釋放一個Unicode格式的字符串指針爷速,可能返回一個字符串的副本
GetStringUTFChars
ReleaseStringUTFChars 獲得/釋放一個UTF-8格式的字符串指針霞怀,可能返回一個字符串的副本
GetStringLength 返回Unicode格式字符串的長度
GetStringUTFLength 返回UTF-8格式字符串的長度
NewString 根據Unicode格式的C字符串創(chuàng)建一個Java字符串
NewStringUTF 根據UTF-8格式的C字符串創(chuàng)建一個Java字符串
GetStringCritical
ReleaseStringCritical 獲得/釋放一個Unicode格式的字符串指針,可能返回一個字符串的副本【在該函數對區(qū)間內毙石,不能使用任何JNI函數】
GetStringRegion 將Unicode格式的String復制到預分配的緩沖區(qū)中
GetStringUTFRegion 將UTF-8格式的String復制到預分配的緩沖區(qū)中
int sprintf( char *buffer, const char *format, [ argument] … );
類似于printf禁谦,根據格式化字符串format废封,將后續(xù)參數列表中的參數逐個輸出。不過輸出目標不是標準輸出終端漂洋,而是字符串buffer。
字符串操作
C字符串——>java字符串
例如:下面的函數以一個C字符串為參數演训,并返回一個Java字符串引用類型jstring值贝咙。
jstring javastring
javastring = (*env)->NewStringUTF(env, "I LOVE YOU !");
注意,在內存溢出的情況下庭猩,NewString函數將返回NULL以通知原生代碼虛擬機中有異常拋出。
java字符串轉換成C字符串
為了在原生代碼中使用java字符串震糖,需要先將java字符串轉換成C字符串趴腋,我們使用GetStringChars函數可以將Unicode格式的java字符串轉換成C字符串,使用GetStringUTFChars函數可以將UTF-8格式的Java字符串轉換成C字符串优炬。這些函數的第三個參數均為可選參數,該可選參數名是isCopy雅宾,它讓調用者確定返回的C字符串地址指向副本還是指向堆中的固定對象糊余。例如:
const jbyte* str;
jboolean isCopy;
str = (*env)->GetStringUTFChars(env, javaString,&isCopy);
if(0 != str){
printf("java String: %s",str);
if(JNI_TRUE == isCopy){
printf("C String is a copy of the java String");
}else{
printf("C String points to actual String");
}
}
釋放字符串
通過JNI GetStringChars 函數GetStringUTFChars函數獲得的C字符串在原生代碼中使用完成之后需要正確的釋放,否則將會引起內存泄漏贬芥。通常我們使用ReleaseStringChars函數釋放Unicode格式的字符串,使用ReleaseUTFStringChars函數釋放UTF-8格式的字符串.
(*env)->ReleaseUTFStringChars(env,javaString,str);
數組操作
JNI把java數組當成引用類型來處理昏苏,JNI提供必要的函數訪問和處理Java數組。
創(chuàng)建數組
用NewArray函數在原生代碼中創(chuàng)建數組實例贤惯,其中可以是Int、Char和Boolean等屁商。例如:
jintArray javaArray;
javaArray = (*env)->NewIntArray(env,10);
if(0 != javaArray){
/*數組使用……*/
}
注意颈墅,在內存溢出的情況下,NewArray函數將返回NULL以通知原生代碼虛擬機中有異常拋出恤筛。
訪問數組元素
JNI提供兩種訪問java數組元素的方法,可以將數組的代碼賦值成C數組或者讓JNI提供直接執(zhí)行數組元素的指針望伦。
對副本的操作
1.java數組轉C數組
GetArrayRegion函數將給定的基本Java數組賦值到給對你給的C數組中煎殷,例如:
jint nativeArray[10];
(*evn)->GetIntArrayRegion(env,javaArray,0,10,nativeArray);
2.C數組轉java數組
原生代碼可以像使用普通的C數組一樣使用和修改數組元素。當原生代碼想將所做的修改提交給java數組時蝌数,可以使用SetArrayRegion函數將C數組復制回java數組中度秘。例如:
(*env)->SetIntArrayRegion(env,javaArray,0,10,nativeArray);
注意:當數組很大時,對數組做復制操作會引起性能問題剑梳。
對直接指針的操作
3.java數組轉C數組
原生代碼可以使用GetArrayElements函數獲取執(zhí)行數組元素的直接指針。例如:
jint nativeDirectArray;
jboolean isCopy;
nativeDirectArray = (*env)->GetIntArrayElements(env,javaArray,&isCopy);
其中垢乙,第三個&isCopy參數為可選參數,讓調用者確定返回的C字符串地址指向副本還是指向堆中的固定對象追逮。
因為可以像普通的C數組一樣訪問和處理數組元素,因此JNI沒提供訪問和處理數組元素的方法骂倘,JNI要求原生代碼用完這些指針后立刻釋放巴席,否則會出現內存溢出。可以使用JNI提供的ReleaseArrayElements函數釋放GetArrayElements返回的C數組堰塌。例如:
(*env)->ReleaseIntArrayRegion(env,javaArray,nativeDirectArray,0);
其中第四個參數是釋放模式分衫。
釋放模式動作0將內容復制回來并釋放原生數組JNI_COMMIT將內容復制回來但是不釋放原生數組,一般用于周期性的更新一個java數組JNI_ABORT釋放原生數組但不用將內容復制回來
NIO操作
JNI提供了在原生代碼中使用NIO(I/O)的函數蚪战,與數組操作相比更適合原生代碼和java應用程序之間傳送大量數據。
創(chuàng)建直接字節(jié)緩沖區(qū)
原生代碼可以創(chuàng)建java應用程序使用的直接字節(jié)緩沖區(qū)施籍,該過程是以提供一個原生C字節(jié)數組為基礎概漱,例如:
unsigned char* buffer = (unsigned char*) malloc(1024)
……
jobject directBuffer;
directBuffer = (*env)->NewDirectByteBuffer(env,buffer,1024);
直接字節(jié)緩沖區(qū)獲取
java應用程序中也可以創(chuàng)建直接字節(jié)緩沖區(qū),在原生代碼中調用GetDirectBufferAddress函數可以獲取原生自己數組的內存地址瓤摧。例如:
unsigned char* buffer
buffer = (unsigned char*) (*env)->GetDirectBufferAddress(env,directBuffer);
JNI訪問java對象屬性
// 實例域
private String instanceField = "Instance Field ";
// 靜態(tài)域
private static String staticField = "Static Field ";
獲取域ID
JNI提供了用域ID訪問兩類域的方法照弥,可以通過給定實例的class對象獲取域ID,用GetObjectClass函數可以獲得class對象这揣,例如:
jclass clazz
clazz = (*env)->GetObjectClass(env,instance);
1.使用GetFieldID獲取實例域的ID
jfieldID instanceFieldId;
instanceField = (*env)->GetFieldID(env,clazz,"instanceFieldId","Ljava/lang/String;");
2.使用GetStaticFieldID獲取靜態(tài)域的ID
jfieldID staticFieldId;
staticFieldId = (*env)->GetStaticFieldID(env,clazz,"staticFieldId","Ljava/lang/String;");
獲取域
在獲得域ID之后,可以用GetField函數獲得實際的實例域机打,例如:
1.獲得實例域
jstring instanceFieldId;
instanceField = (*env)->GetObjectField(env,clazz,"instanceFieldId");
2.獲得靜態(tài)域
jfieldID staticField;
staticFieldId = (*env)->GetStaticObjectField(env,clazz,"staticFieldId");
兩個函數的最后一個參數是java中表示域類型的域描述符片迅,其中”Ljava/lang/String;”表明域類型是Sting。
JNI調用Java方法
public class WJavaClass{
// 實例方法
private String instanceMethod(){
return "Instance Method";
}
// 靜態(tài)方法
private static String staticMethod(){
return "StaticMethod";
}
}
獲取方法ID
JNI提供了用方法ID訪問兩類方法的途徑芥挣,可以用給定實例的class對象獲得方法ID耻台。用GetMethodID函數獲得實例方法的方法ID,例如:
jmethodID instanceMethodId;
instanceMethodId = (*env)->GetMethodID(env,clazz,"instanceMethod","()Ljava/lang/String;");
用GetStaticMethodID函數獲得靜態(tài)域的方法ID粘我,例如:
jmethodID staticMethodId;
staticMethodId = (*env)->GetStaticMethodID(env,clazz,"staticMethod","()Ljava/lang/String;");
兩個函數的最后一個參數均表示方法描述符痹换,在Java中表示方法簽名都弹。
調用方法
可以以方法ID為參數通過CallMethod類函數調用實際的實例方法,例如:
1.調用實例方法
jstring instanceMethodResult;
instanceMethodResult = (*env)->CallStringMethod(env,instance,"instanceMethodId");
2.調用靜態(tài)方法
jstring staticMethodResult;
staticMethodResult = (*env)->CallStaticStringMethod(env,clazz,"staticMethodId");
JNI調用Java靜態(tài)方法案例
public class HelloJni extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
callJavaStaticMethod();
}
public native String callJavaStaticMethod();
static {
System.loadLibrary("hello-jni");
}
// 靜態(tài)方法
private static String staticMethod() {
return "StaticMethod Castiel";
}
}
#include <string.h>
#include <jni.h>
#include <android/log.h>
JNIEXPORT void JNICALL
Java_com_example_hellojni_HelloJni_callJavaStaticMethod(JNIEnv *env, jclass type) {
jclass jniClass = (*env)->FindClass(env, "com/example/hellojni/HelloJni");
if (NULL == jniClass) {
__android_log_print(ANDROID_LOG_INFO,"HelloJni","can't find jclass");
return;
}
jmethodID getMId = (*env)->GetStaticMethodID(env, jniClass, "staticMethod",
"()Ljava/lang/String;");
if (NULL == getMId) {
__android_log_print(ANDROID_LOG_INFO,"HelloJni","can't find method getStringFromStatic from JniClass");
return;
}
jstring result = (*env)->CallStaticObjectMethod(env, jniClass, getMId);
const char *resultChar = (*env)->GetStringUTFChars(env, result, NULL);
(*env)->DeleteLocalRef(env, jniClass);
(*env)->DeleteLocalRef(env, result);
__android_log_print(ANDROID_LOG_INFO,"HelloJni",resultChar);
JNI異常處理
調用throwingMethod方法時,accessMethod原生方法需要顯示地做異常處理框杜。JNI提供了ExceptionOccurred函數查詢虛擬機中是否有掛起的現象。例如振劳,原生代碼中的異常處理:
jthrowable ex;
……
(*env)->CallVoidMethod(env,instance,throwingMethodId);
ex = (*env)->ExceptionOccurred(env);
if(0 != ex){
(*env)->ExceptionClear(env);
/*Exception handler*/
}
拋出異常
public class WJavaClass{
// 拋出方法
private void throwingMethod() throws NullPointerException{
throw new NullPointerException("Null Pointer");
}
}
JNI也允許原生代碼拋出異常油狂。因為異常是java類,應該先用FindClass函數找到異常類专筷。用ThrowNew函數可以初始化且拋出新的異常,例如:
jclass clazz;
……
clazz = (*env)->FindClass(env,"java/lang/NullPointerException");
if(0 !=clazz){
(*env)->ThrowNew(env,clazz,"Exception message");
}
JNI的局部引用和全局引用和弱全局引用
局部引用
大多數JNI函數返回局部引用吮旅。局部引用不能在后續(xù)的調用中被緩存及重用味咳,主要是因為它們的使用期限僅限于原生方法,一旦原生函數返回莺葫,局部引用即被釋放。例如,使用FindClass函數返回一個局部引用贸铜,當原生方法返回時,它被自動釋放烤镐,也可以用DeleteLocalRef函數顯示釋放原生代碼:
jclass clazz
clazz = (*env)->FindClass(env,"java/lang/String");
……
(*env)->DeleteLocalRef(env,clazz);
根據JNI的規(guī)范棍鳖,虛擬機應該允許原生代碼創(chuàng)建最少16個局部引用
全局引用
全局引用在原生方法的后續(xù)調用過程中依然有效碗旅,除非它們被原生代碼顯示釋放镜悉。
1.創(chuàng)建全局引用
可以用NewGlobalRef函數將局部引用初始化為全局引用,例如:
jclass localclazz
jclass globalclazz
……
localclazz = (*env)->FindClass(env,"java/lang/String");
globalclazz = (*env)->NewGlobalRef(env,localclazz );
……
(*env)->DeleteLocalRef(env,localclazz );
2.刪除全局引用
當原生代碼不再需要一個全局引用時旧困,可以隨時用DeleteLocalRef函數釋放它。
(*env)->DeleteLocalRef(env,globalclazz );
弱全局引用
弱全局引用和全局引用一樣吼具,在原生方法的后續(xù)調用過程中依然有效矩距。與全局引用不同,弱全局引用并不阻止?jié)撛诘膶ο蟊焕厥铡?br>
1.創(chuàng)建弱全局引用
用NewWeakGlobalRef函數對弱全局引用進行初始化锥债,例如:
jclass weakGlobalclazz
weakGlobalclazz = (*env)->NewWeakGlobalRef(env,localclazz);
2.弱全局引用的有效性校驗
可以使用IsSameObject函數檢驗一個弱全局引用是否仍然指向活動的類實例,例如:
if(JNI_FALSE == (*env)->IsSameObject(env,weakGlobalClazz,NULL)){
/*對象仍然處于活動狀態(tài)且可以使用*/
}else{
/*對象被垃圾回收期收回毅整,不能使用*/
}
刪除弱全局引用
可以隨時使用DeleteWeakGlobalRef函數釋放弱全局引用绽左。
(*env)->DeleteLocalRef(env,weakGlobalClazz);