1. 在Java層利用JNI調用Native層代碼
如果有Java層嘗試調用Native層的代碼,我們通常用Java對象來封裝C++的對象。舉個例子,在Java層的一個監聽播放狀態的類:MusicPlayListener,作用是將播放狀態發送給位於Native層的Cocos,通知Cocos在界面上修改顯示圖標,例如“播放”,“暫停”等等。
第一種做法,是在Java類的構造函數中,調用Native層的構造函數,分配Native Heap的內存空間,之後,在Java類的finalize方法中調用Native層的析構函數,回收Native Heap的內存空間。
// in Java: public class MusicPlayListener { // 指向底層對象的指針,僞裝成Java的long private final long ptr; public MusicPlayListener() { ptr = ccCreate(); } // 在finalize裏釋放 public void finalize() { ccFree(ptr); } // 是否正在播放 public void setPlayState(boolean isPlaying){ ccSetPlayState(ptr,isPlaying); } private static native long ccCreate(); private static native void ccFree(long ptr); private native void ccSetPlayState(long ptr,boolean isPlaying); } // in C: jlong Java_MusicPlayListener_ccCreate(JNIEnv* env, jclass unused) { // 調用構造函數分配內存空間 CCMusicPlayListener* musicPlayListener = new CCMusicPlayListener(); return (jlong) musicPlayListener; } void Java_MusicPlayListener_ccFree( JNIEnv* env, jclass unused, jlong ptr) { // 釋放內存空間 delete ptr; } void Java_MusicPlayListener_ccSetPlayState( JNIEnv* env, jclass unused, jlong ptr, jboolean isPlaying) { //將播放狀態通知給UI線程 (reinterpret_cast<CCMusicPlayListener*>(ptr))->setPlayState(isPlaying); }
這種做法會讓Java對象和Native對象的生命週期保持一致,當Java對象在Java Heap中,被GC判定爲回收時,同時會將Native Heap中的對象回收。
不通過finalize的話,也可以用其他類似的機制適用於上述場景。比如Java標準庫提供的DirectByteBuffer的實現,用基於PhantomReference的sun.misc.Cleaner來清理,本質上跟finalize方式一樣,只是比finalize稍微安全一點,他可以避免”懸空指針“的問題。
這種方式的一個重要缺點,就是不管是finalize還是其他類似的方法,都依賴於JVM的GC來處理的。換句話說,如果不觸發GC,那麼finalize方法就不會及時調用,這可能會導致Native Heap資源耗盡,而導致程序出錯。當Native層需要申請一個很大空間的內存時,有一定機率出現Native OutOfMemoryError的問題,然後找了半天也發現不了問題在哪裏...
第二種方法是對Api的一些簡單調整,以解決上述問題。不在JNI的包裝類的構造函數中初始化Native層對象,儘量寫成open/close的形式,在open的時候初始化Native資源,close的時候釋放,finalize作爲最後的保險再檢查釋放一次。
雖然沒有本質上的變化,但open/close這種Api設計,一般來說,對90%的開發人員還是能夠提醒他們使用close的,至於剩下的10%...好像除了開除也沒啥好辦法了...
2. 在Native層利用JNI調用Java層代碼
上一種情況,是以Java層爲主導,Native層對象的生命週期受Java層對象的控制。下面要介紹的是另一種情況,即Native層對象爲主導,由他控制Java層對象的生命週期。
2.1 Native層操作Java層對象
想要在native層操作Java Heap中的對象,需要位於Native層的引用(Reference)以指向Java Heap中的內存空間。JNI中爲我們提供了三種引用:本地引用(Local Reference),全局引用(Global Reference)和弱全局引用(Weak Global Reference)。
Local Reference的生命週期持續到一個Native Method的結束,當Native Method返回時Java Heap中的對象不再被持有,等待GC回收。一定要注意不要在Native Method中申請過多的Local Reference,每個Local Reference都會佔用一定的JVM資源,過多的Local Reference會導致JVM內存溢出而導致Native Method的Crash。但是有些情況下我們必然會創建多個Local Reference,比如在一個對列表進行遍歷的循環體內,這時候開發人員有必要調用DeleteLocalRef手動清除不再使用的Local Reference。
//C++代碼 class Coo{ public: void Foo(){ //獲得局部引用對象ret jobject ret = env->CallObjectMethod(); for(int i =0;i<10;i++){ //獲得局部引用對象cret jobject cret = env->CallObjectMethod(); //... //手動回收局部引用對象cret env->DeleteLocalRef(cret); } } //native method 返回,局部引用對象ret被自動回收 };
Global Reference的生命週期完全由程序員控制,你可以調用NewGlobalRef方法將一個Local Reference轉變爲Global Reference,Global Reference的生命週期會一直持續到你顯式的調用DeleteGlobalRef,這有點像C++的動態內存分配,你需要記住new/delete永遠是成對出現的。
//C++代碼 class Coo{ public: void Foo(){ //獲得局部引用對象ret jobject ret = env->CallObjectMethod(); //獲的全局引用對象gret jobject gret = env->NewGlobalRef(ret); }//native method 返回,局部引用對象ret被自動回收 //gret不會回收,造成內存溢出 };
Weak Global Reference是一種特殊的Global Reference,它允許JVM在Java Heap運行GC時回收Native層所持有的Java對象,前提是這個對象除了Weak Reference以外,沒有被其他引用持有。我們在使用Weak Global Reference之前,可以使用IsSameObject來判斷位於Java Heap中的對象是否被釋放。
2.2 Native層釋放的同時釋放Java層對象
C++中的對象總會在其生命週期結束時,調用自身的析構函數,釋放動態分配的內存空間,Cocos利用資源釋放池(其本質是一種引用計數機制)來管理所有繼承自cocos2d::CCObject(3.2版本之後變爲cocos::Ref)的對象。換言之,對象的生命週期交給Cocos管理,我們需要關心對象的析構過程。
一種簡單有效的做法,是在C++的構造函數中,實例化Java層的對象,在C++的析構函數中釋放Java層對象。舉個例子,主界面需要拉取Java層代碼來解析後臺協議,獲取到主界面的幾個圖片的URL信息。
代碼:
//C++代碼 class CCMainDeskListener { public: CCMainDeskListener(); ~CCMainDeskListener(); private: //Java層對象的全局引用 jobject retGlobal; }; CCMainDeskListener::CCMainDeskListener() { //獲得本地引用 jobject ret = CallStaticObjectMethod(); //創建全局引用 retGlobal = NewGlobalRef(ret); //清除本地引用 DeleteLocalRef(ret); } CCMainDeskListener::~CCMainDeskListener() { //清除全局引用 DeleteGlobalRef(retGlobal); }
在C++的構造函數中,調用Java層的方法初始化了Java對象,這個引用分配的內存空間位於Java Heap。之後我們創建全局引用,避免Local Reference在Native Method結束之後被回收,而全局引用在析構函數中被刪除,這樣就保證了Java Heap中的對象被釋放,保持Native層和Java層的釋放做到同步。
上述方法中,Java層對象的生命週期是跟隨Native層對象的生命週期的,Native層對象的生命週期結束時會釋放對於Java層對象的持有,讓GC去回收資源。我們想進一步瞭解Native層對象的什麼時候被回收,接下來介紹一下Cocos的內存管理策略。
3.Cocos的內存管理
C++中,在堆上分配和釋放動態內存的方法是new和delete,程序員要小心的使用它們,確保每次調用了new之後,都有delete與之對應。爲了避免因爲遺漏delete而造成的內存泄露,C++標準庫(STL)提供了auto_ptr和shared_ptr,本質上都是用來確保當對象的生命週期結束時,堆上分配的內存被釋放。
Cocos採用的是引用計數的內存管理方式,這已經是一種十分古老的管理方式了,不過這種方式簡單易實現,當對象的引用次數減爲0時,就調用delete方法將對象清除掉。具體實現上來說,Cocos會爲每個進程創建一個全局的CCAutoreleasePool類,開發人員不能自己創建釋放池,僅僅需要關注release和retain方法,不過前提是你的對象必須要繼承自cocos2d::CCObject類(3.0版本之後變爲cocos2d::Ref類),這個類是Cocos所有對象繼承的基類,有點類似於Java的Object類。
當你調用object->autorelease()方法時,對象就被放到了自動釋放池中,自動釋放池會幫助你保持這個obejct的生命週期,直到當前消息循環的結束。在這個消息循環的最後,假如這個object沒有被其他類或容器retain過,那麼它將自動釋放掉。例如,layer->addChild(sprite),這個sprite增加到這個layer的子節點列表中,他的聲明週期就會持續到這個layer釋放的時候,而不會在當前消息循環的最後被釋放掉。
跟內存管理有關的方法,一共有三個:release(),retain()和autorelease()。release和retain的作用分別是將當前引用次數減一和加一,autorelease的作用則是將當前對象的管理交給PoolManager。當對象的引用次數減爲0時,PoolManager就會調用delete,回收內存空間。
release和retain的作用分別是將當前引用次數減一和加一,autorelease的作用則是將當前對象的管理交給PoolManager。當對象的引用次數減爲0時,PoolManager就會調用delete,回收內存空間。
一般情況下,我們需要記住的就是繼承自Ref的對象,使用create方法創建實例後,是不需要我們手動delete的,因爲create方法會自己調用autorelease方法。
4.總結
JNI調用時,即可能造成Native Heap的溢出,也可能造成Java Heap的溢出,作爲JNI軟件開發人員,應該注意以下幾點:
- Native層(一般是C++)本身的內存管理。
- 不使用的Global Reference和Local Reference都要及時釋放。
- Java層調用JNI時儘量使用open/close的格式替代構造函數/finalize的方式。