1. 程式人生 > >JNI字串,陣列,欄位和方法,物件引用,異常處理,多執行緒,記憶體回收等

JNI字串,陣列,欄位和方法,物件引用,異常處理,多執行緒,記憶體回收等

  JNI中字串,陣列,欄位和方法,區域性引用和全域性引用,異常處理,多執行緒等核心內容。

-- finalize的作用:
 1.finalize()是Object的protected方法,子類可以覆蓋該方法以實現資源清理工作,GC在回收物件之前呼叫該方法。
 2.finalize()與C++中的解構函式不是對應的。C++中的解構函式呼叫的時機是確定的(物件離開作用域或delete掉),但Java中的finalize的呼叫具有不確定性

 3.不建議用finalize方法完成“非記憶體資源”的清理工作,但建議用於:① 清理本地物件(通過JNI建立的物件);② 作為確保某些非記憶體資源(如Socket、檔案等)釋放的一個補充:在finalize方法中顯式呼叫其他資源釋放方法。其原因可見下文[finalize的問題].

JNI與JVM的聯絡。

> JNI 物件引用;區域性引用 全域性引用 弱全域性引用

  JNI區域性引用(Local Reference)、全域性引用(Global Reference)、弱全域性引用(Weak Global Reference)
-- JNI 中區域性引用和全域性引用- https://blog.csdn.net/tonyfield2015/article/details/8803380
 JNI給出例項和陣列型別(如jobject,jclass,jstring,jarray)作為不透明的引用。Native程式碼從不會直接訪問一個不透明的引用的內容。相反,它使用JNI函式訪問由一個不透明的參考指向資料結構。只處理不透明的引用,你不用擔心依賴於特定的Java虛擬機器實現的內部物件佈局。但是你的確需要進一步瞭解JNI中不同型別的引用:
 1.JNI 支援三種不透明引用:區域性引用( local references),全域性引用(global references)和弱全域性引用( weak global references)。
 2.區域性和全域性引用有不同的生命週期,本地應用被自動釋放,而全域性應用和弱全域性引用僅在顯式呼叫釋放命令時釋放。
 3.一個區域性和全域性引用保持引用物件不被garbage記憶體回收。另一方面,弱全域性引用是允許引用物件被garbage記憶體回收的。
 4.並非所有的引用都能在任何上下文中被使用。例如,在建立引用的native函式返回後使用這個引用是不合法的。

 有兩種方法使一個區域性引用無效。如前所述,當native方法返回後,虛擬機器自動釋放了方法執行期間建立的所有區域性引用。另外,程式設計師可以用像DeleteLocalRef 這樣的JNI函式顯式管理區域性引用的生命週期。既然在native方法返回後虛擬機器會自動釋放期間建立的所有區域性引用,那為什麼需要顯式刪除區域性引用呢? 一個區域性引用直到它無效時才會讓garbage收集引用物件
 區域性引用也僅在建立它們的執行緒中有效。在一個執行緒中建立的區域性引用不能為另一個執行緒所用。在native 方法中儲存一個區域性引用到全域性變數並期望在另一個執行緒中使用它是錯誤的。

   你能在一個native方法的多個呼叫間使用一個全域性引用。一個全域性引用也能在多執行緒件被使用,直到被程式設計師顯式釋放。類似區域性引用,一個全域性引用在被釋放前保證引用物件不被garbage回收。
   和區域性引用不同的是,沒有那麼多函式能夠建立全域性引用。能建立全域性引用的函式只有 NewGlobalRef。
   弱全域性引用是 Java 2 SDK r1.2中新增的。弱全域性引用用 NewGlobalWeakRef 建立,用 DeleteGlobalWeakRef 釋放。類似全域性引用,弱全域性引用在native方法和不同執行緒間保持有效。與之不同的是,弱全域性引用不會阻止底層物件被garbage 收集。 
   Java 2 SDK r1.2 提供另一系列函式來管理區域性引用的生命週期。這些函式是 EnsureLocalCapacity,New-LocalRef,PushLocalFrame,和 PopLocalFrame.如果需要建立更多的區域性引用,一個native方法可能呼叫 EnsureLocalCapacity 來保證足夠數量的區域性引用有效。Push/PopLocalFrame 函式讓程式設計師可以建立作用域巢狀的區域性引用。PushLocalFrame 為特定數量的區域性引用建立一個新的作用域。 PopLocalFrame 銷燬最頂層作用域,釋放這個作用域中的所有區域性引用。使用 Push/PopLocalFrame 函式的優點是,他們是管理區域性引用的生命週期成為可能,不必擔心執行期間被建立的每個單個區域性引用。當 native程式碼不再需要訪問一個全域性引用,你應當呼叫 DeleteGlobalRef 。
  如果沒有成功呼叫這個函式,即使物件不再系統任何地方被使用的情況,Java虛擬機器garbage也不會收集相應物件。    當 native程式碼不再需要訪問一個弱全域性引用,你應當呼叫 DeleteWeakGlobalRef。 如果沒有成功呼叫這個函式,Java虛擬機器garbage也會收集這個物件。但弱全域性引用本身消耗的記憶體將無法回收。

-- JNI 區域性引用、全域性引用和弱全域性引用- http://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/recommend.html
一般有兩種 native 程式碼:直接實現 native 方法的函式 和 在任何上下文中使用的工具函式。
    當編寫直接實現 native 方法的函式,你需要小心迴圈中的過度建立區域性引用,以及native 方法中不需要返回的不必要的區域性引用。對於虛擬機器,用上16個以內的區域性引用待native方法返回後刪除是可以接受的。Native 方法呼叫一定不能引起全域性引用或弱全域性引用的累積,因為全域性引用和弱全域性引用不會在native方法返回後自動釋放。
  在編寫native工具函式時你必須注意不要造成執行路徑中任何的區域性引用洩露。因為一個工具函式可能在無法預料的情況下被重複呼叫,任何沒有必要建立的引用可能造成記憶體溢位。
 當呼叫一個返回原始型別的工具函式,它一定不能有累積各型別引用的副作用。
 當呼叫一個返回引用型別的工具函式,它一定不能累積各型別引用,除了作為結果返回的那個引用。
 JNI 引用使用不當造成引用表溢位,最終導致程式崩潰。

 啟動一個 Java 程式,如果沒有手動建立其它執行緒,預設會有兩個執行緒在跑,一個是 main 執行緒,另一個就是 GC 執行緒(負責將一些不再使用的物件回收)。
  在 C++ 中 new 一個物件,使用完了還要做一次 delete 操作,malloc 一次同樣也要呼叫 free 來釋放相應的記憶體,否則你的程式就會有記憶體洩露了。
  在 C/C++ 中記憶體還分棧空間和堆空間,其中區域性變數、函式形參變數、for 中定義的臨時變數所分配的記憶體空間都是存放在棧空間(而且還要注意大小的限制),用 new 和 malloc 申請的記憶體都存放在堆空間。但 C/C++ 裡的記憶體管理還遠遠不止這些,這些只是最基礎的記憶體管理常識。

> JNI多執行緒

JNI DETECTED ERROR IN APPLICATION: can't call void XXX on instance of java.lang.Class <XXX>解決方案-https://blog.csdn.net/oMrLeft123/article/details/54601191  問題1:
JNIEnv是一個執行緒相關的變數;JNIEnv 對於每個 thread 而言是唯一的 ;JNIEnv *env指標不可以為多個執行緒共用
解決辦法:
但是java虛擬機器的JavaVM指標是整個jvm公用的,我們可以通過JavaVM來得到當前執行緒的JNIEnv指標.可以使用javaAttachThread保證取得當前執行緒的Jni環境變數
static JavaVM *gs_jvm=NULL;
gs_jvm->AttachCurrentThread((void **)&env, NULL);//附加當前執行緒到一個Java虛擬機器
jclass cls = env->GetObjectClass(gs_object);
jfieldID fieldPtr = env->GetFieldID(cls,"value","I");
  問題2:
不能直接儲存一個執行緒中的jobject指標到全域性變數中,然後在另外一個執行緒中使用它。
解決辦法:
用env->NewGlobalRef建立一個全域性變數,將傳入的obj(區域性變數)儲存到全域性變數中,其他執行緒可以使用這個全域性變數來操縱這個java物件
注意:若不是一個 jobject,則不需要這麼做。如:
jclass 是由 jobject public 繼承而來的子類,所以它當然是一個 jobject,需要建立一個 global reference 以便日後使用。
而 jmethodID/jfieldID 與 jobject 沒有繼承關係,它不是一個 jobject,只是個整數,所以不存在被釋放與否的問題,可儲存後直接使用。
static jobject gs_object=NULL;
JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)
{
    env->GetJavaVM(&gs_jvm); //儲存到全域性變數中JVM 
    //直接賦值obj到全域性變數是不行的,應該呼叫以下函式: 
    gs_object=env->NewGlobalRef(obj);

}

-- 子執行緒函式裡需要使用AttachCurrentThread()和DetachCurrentThread()這兩個函式。
1.在JNI_OnLoad中,儲存JavaVM*,這是跨執行緒的,持久有效的,而JNIEnv*則是當前執行緒有效的。一旦啟動執行緒,用AttachCurrentThread方法獲得env。
2.通過JavaVM*和JNIEnv可以查詢到jclass。
3.把jclass轉成全域性引用,使其跨執行緒。
4.然後就可以正常地呼叫你想呼叫的方法了。
5.用完後,別忘了delete掉建立的全域性引用和呼叫DetachCurrentThread方法。

JNI的監視器允許原生程式碼利用Java物件同步,虛擬機器保證存取監視器的執行緒能夠安全執行,而其他執行緒等待監視器物件程式設計可用狀態,例如:
synchronized(obj){
  /*同步執行緒安全程式碼塊*/
}
在原生程式碼中相同級別同步可以用JNI的監視器方法實現的,例如:
if(JNI_OK == (*env)->MonitorEnter(env,obj)){
  /*錯誤處理*/
}
/*同步執行緒安全程式碼塊*/
if(JNI_OK == (*env)->MonitorExit(env,obj)){
  /*錯誤處理*/
}
/*將當前執行緒附著到虛擬機器上*/
(*cachedJvm)->AttachCurrentThread(cachedJvm,&env,NULL);
/*可以用JNIEnv介面實現執行緒與java應用程式的通訊*/
/*將當前執行緒與虛擬機器分離*/
(*cachedJvm)->DetachCurrentThread(cachedJvm);


> JNI記憶體回收
JNI引用與垃圾回收- http://blog.csdn.net/ordinaryjoe/article/details/7666571
evn->ReleaseStringUTFChars(signature, releaseMD5);
env->ReleaseByteArrayElements(objArraySign, byte_array_elements, JNI_ABORT);
  JNI是Android系統中底層和框架層通訊的重要方式、JNI對於Android安全以及Android安全加固等都是有所幫助的。C語言中呼叫Java的一些函式,實際上也是反射獲取的,步驟跟Java層的是一樣的,換句話說在Java反射能做到的,在JNI中通過類似的反射也是可以做到的。
-- android jni 記憶體洩露- http://blog.csdn.net/liwei405499/article/details/42024773
JNI(java native interface)經常遇到到問題:
  問題1.  忘記delete local reference。帶New到方法(如:NewByteArray)這樣到方法比較好辨認,需要手動呼叫DeleteLocalRef()來釋放(返回值除外)。比較特殊的一個方法是:GetByteArrayELement必須要呼叫ReleaseByteArrayElements進行釋放。當然如果你只是取bytearray中到byte,那麼完全可以用GetByteArrayRegion實現。
  問題2. 沒有NewGlobalRef。 在不同執行緒呼叫java方法,需要儲存jobject物件,這時需要對jobject物件做全域性引用,否則會失效。
  問題3.  jbytearray的length。在JNI layer獲取到jbytearray到長度是不對到,應該由java獲取byte[]的length再傳給C layer。否則C layer有可能獲得到是亂碼。
  問題4.  執行緒問題。 不同執行緒使用JNIEnv*物件,需要AttachCurrentThread將env掛到當前執行緒,否則無法使用env。
  問題5.  javap 命令是對java的class檔案操作;而javah命令需要在包名到上一層路徑執行才行,否則無法生成.h檔案。
  問題6. 儘量避免頻繁呼叫JNI或者是使用JNI傳輸大量到資料。
  問題7. Reference Table overflow (max=1024) 或者是 Reference Table overflow (max=512)一定是因為忘記釋放global reference或者local reference,請仔細檢查程式碼。
  問題8. 不要在windows下使用cygwin編譯NDK code,那樣會遇到arguments too long問題,因為windows路徑長度有限制導致。雖然可以使用subst將路徑對映為短路徑,但是在編譯時間和除錯上,windows的孩子都是傷不起。同樣到build,在windows下要15分鐘左右,而在mac下只要5分多,相差3倍。除錯JNI 程式碼到速度更是不用提了,差太多。

  總結,JNI程式碼量其實不是很多,JNI作為一個數據傳輸層,它的作用僅僅是java和c直接到橋樑,但是如果處理不好將會是災難,除錯和找bug非常困難。

>JNI異常

  在Android中我們處理異常的方式一般都是:發現異常、捕獲異常(向上層丟擲異常)、處理異常。JNI中對於異常的處理和Andrid很相似。基本的流程都是檢查異常,捕獲異常,丟擲異常,處理異常。 JNI 程式設計的記憶體管理。

Android jni/ndk程式設計五:jni異常處理- https://blog.csdn.net/u011913612/article/details/52668422
-- Java、Dalvik VM、C/C++的執行機制與流程 在Android的NDK中,Java、C/C++、Dalvik VM關係如下:
 1、java的dex位元組碼和C/C++的*.so同時執行DalvikVM之內,共同使用一個程序空間。每次使用jni呼叫c/c++開闢一個執行緒去處理
 2、java和C/C++可以相互呼叫,呼叫的關鍵是DalvikVM
 3、一般而言,比較經典的模式是Java通過JNI的C組建和C++相互溝通,一般業務處理放在C/C++中
 4、C++程式碼處於核心控制地位更具價值

 當java需要C/C++程式碼時,在DalvikVM虛擬機器中載入動態連結庫時,會先呼叫JNI_Onload()函式,此時就會把javaVM物件的指標儲存於C層JNI組建的全域性環境中,在JAVA層呼叫C層的本地庫函式時,呼叫C本地函式執行緒必然通過Dalvik VM來呼叫C本地函式,測試Dalvik虛擬機器會為本地的C組建例項化一個JNIEnv指標,該指標指向Dalvik虛擬機器的具體函式列表,當JNI的C元件呼叫java層方法和屬性時,需要通過JNIEnv指標來進行呼叫。
 當C++元件主動呼叫Java層方法時,需要通過JNI的C元件把JNIEnv指標傳遞給C++元件,此後,c++元件即可通過JNIEnv指標來掌控Java層程式碼。

--- 對於JNI和NDK很多Android開發初學者沒有搞明白這個問題: 
- JNI是Java呼叫Native機制,是Java語言自己的特性全稱為Java Native Interface 
- 類似的還有微軟.Net Framework上的p/invoke,可以讓C#或Visual Basic.NET可以呼叫C/C++的API,所以說JNI和Android沒有關係 
- 在PC上開發Java的應用,如果執行在Windows平臺使用JNI是是經常的,比如說讀寫Windows的登錄檔。- 而NDK是Google公司推出的幫助Android開發者通過C/C++本地語言編寫應用的開發包,包含了C/C++的標頭檔案、庫檔案、說明文件和示例程式碼 
- 我們可以理解為Windows Platform SDK一樣,是純C/C++編寫的,但是Android並不支援純C/C++編寫的應用 
- 同時NDK提供的庫和函式功能很有限,僅僅處理些演算法效率敏感的問題

 -- 在java的程式設計中,我們經常會遇到各種的異常,也會處理各種的異常。處理異常在java中非常簡單,我們通常會使用try-catch-finally來處理,也可以使用throw簡單丟擲一個異常。Java的異常和native 程式碼的異常或者錯誤處理是不同的機制。實踐中發現,並不是JNI呼叫發生異常時,就會在呼叫函式處進行棧展開,而是在後續的某個JNI呼叫時,才發生異常退出,比如開頭提到的attempt to use stale local reference錯誤,就是因為某個JNI呼叫丟擲了異常,沒有進行處理;結果在後續第N個呼叫時,才提示錯誤並推出,logcat也沒有列印java的異常資訊。這導致問題比較難定位。

 專用的JNI函式,可以對異常進行處理:
Throw():丟棄一個現有的異常物件;在固有方法中用於重新丟棄一個異常。
ThrowNew():生成一個新的異常物件,並將其丟棄。
ExceptionOccurred():判斷一個異常是否已被丟棄,但尚未清除。
ExceptionDescribe():列印一個異常和堆疊跟蹤資訊。
ExceptionClear():清除一個待決的異常。
FatalError():造成一個嚴重錯誤,不返回。

    jint        (*Throw)(JNIEnv*, jthrowable);
    jint        (*ThrowNew)(JNIEnv *, jclass, const char *);
    jthrowable  (*ExceptionOccurred)(JNIEnv*);
    void        (*ExceptionDescribe)(JNIEnv*);
    void        (*ExceptionClear)(JNIEnv*);
    void        (*FatalError)(JNIEnv*, const char*);


1> ExceptionCheck:檢查是否發生了異常,若有異常返回JNI_TRUE,否則返回JNI_FALSE 
2> ExceptionOccurred:檢查是否發生了異常,若用異常返回該異常的引用,否則返回NULL 
3> ExceptionDescribe:列印異常的堆疊資訊 
4> ExceptionClear:清除異常堆疊資訊 
5> ThrowNew:在當前執行緒觸發一個異常,並自定義輸出異常資訊 
6> Throw:丟棄一個現有的異常物件,在當前執行緒觸發一個新的異常 
7> FatalError:致命異常,用於輸出一個異常資訊,並終止當前VM例項(即退出程式)

 在所有這些函式中,最不能忽視的就是ExceptionOccurred()和ExceptionClear()。大多數JNI函式都能產生異常,而且沒有象在Java的try塊內的那種語言特性可供利用。所以在每一次JNI函式呼叫之後,都必須呼叫ExceptionOccurred(),瞭解異常是否已被丟棄。若偵測到一個異常,可選擇對其加以控制(可能時還要重新丟棄它)。然而,必須確保異常最終被清除。這可以在自己的函式中用ExceptionClear()來實現;若異常被重新丟棄,也可能在其他某些函式中進行。

 舉個最簡單的例子,如果呼叫Java中的方法後出現異常,忽略。
jobject objectAttr = (*env)->CallObjectMethod(env, objectDocument, createAttributeMid, stoJstring(env, "ABC"));
// deal with exception
exc = (*env)->ExceptionOccurred(env);
if(exc) {
      (*env)->ExceptionClear(env);
      doSomething();

> JNI 資源釋放
 關於JNI 資源釋放- https://blog.csdn.net/lxb00321/article/details/62881171
一、多次NewByteArray後,報錯“ReferenceTable overflow”
解決辦法:釋放所有對object的引用
例: jbyteArray audioArray = jnienv->NewByteArray(frameSize);
       jnienv->SetByteArrayRegion(audioArray,0,frameSize,(jbyte*)fReceiveBuffer);
       jnienv->DeleteLocalRef(audioArray);

1.FindClass 
例如,jclass ref= (env)->FindClass("java/lang/String");
env->DeleteLocalRef(ref); 
 
2.NewString/ NewStringUTF/NewObject/NewByteArray
例如,jstring     (*NewString)(JNIEnv*, const jchar*, jsize);    
const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);     
void        (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);
jstring     (*NewStringUTF)(JNIEnv*, const char*);    
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);     

void        (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
env->DeleteLocalRef(ref);
 
3.GetObjectField/GetObjectClass/GetObjectArrayElement
jclass ref = env->GetObjectClass(robj);
env->DeleteLocalRef(ref); 
 
4.GetByteArrayElements
jbyte* array= (*env)->GetByteArrayElements(env,jarray,&isCopy);
(*env)->ReleaseByteArrayElements(env,jarray,array,0);
 
5.const char* input =(*env)->GetStringUTFChars(env,jinput, &isCopy);
(*env)->ReleaseStringUTFChars(env,jinput,input);
 
6.NewGlobalRef/DeleteGlobalRef
 jobject     (*NewGlobalRef)(JNIEnv*, jobject);     
 void        (*DeleteGlobalRef)(JNIEnv*, jobject);
例如,jobject ref= env->NewGlobalRef(customObj);
env->DeleteGlobalRef(customObj);