標籤:
Cocos2d-x 3.x記憶體管理機制1:C++記憶體管理1-1:記憶體配置地區
建立對象需要兩個步驟:第一步,為對象分配記憶體;第二步,調用建構函式初始化記憶體。在第一步中,可以選擇幾個不同的分配地區。這幾個地區如下:
(1) 棧地區分配。棧記憶體配置運算內建於處理器的指令集中,效率很髙,但是分配的內 存容量有限。由處理器自動分配和釋放,用來存放函數的參數值和局部變數的值等。在執 行函數時,函數內局部變數的儲存單元都可以在棧上建立,函數執行結束時這些儲存單元自動被釋放。
(2) 堆地區分配。從堆上分配,也稱動態記憶體分配。由開發人員分配釋放,如果不釋 放,程式結束時由作業系統回收。程式在運行時用malloc或new申請任意多少的記憶體,開發人員自己負責在何時用free或delete釋放記憶體。動態記憶體的生存期由開發人員決定,使用非常靈活,但問題也最多。
(3) 在靜態儲存地區分配。這個記憶體空間在程式的整個運行期間都存在,記憶體在程式編譯的時候就已經分配好。它可以分配全域變數和靜態變數。
1-2:動態記憶體分配
動態記憶體分配最為靈活但是問題也很多,這裡重點介紹動態記憶體分配。動態記憶體使用 malloc或new分配記憶體,使用free或delete釋放記憶體。其中,malloc和free是成對的,new 和delete是成對的。
(1)malloc和free的使用
malloc和free是C/C++語言的標準庫函數,主要是在C中使用。使用malloc建立對 象,不會自動調用建構函式初始化記憶體。使用free釋放對象,不會自動調用解構函式清除記憶體。
(2)new和delete的使用
與malloc和free不同,new和delete不是函數庫,而是C++的運算子。new運算子能 夠完成建立對象所有步驟(即第一步,為對象分配記憶體;第二步,調用建構函式初始化內 存),它也會調用建構函式。執行個體代碼如下:
MyObject * obj = new MyObject();
建構函式可以重載,根據使用者傳遞的參數列表,決定調用哪個建構函式進行初始化對象。
new運算子的反操作運算子是delete,delete先調用解構函式,再釋放記憶體。執行個體代碼如下:
delete obj;
其中,obj是對象指標,obj只能釋放new建立的對象,不能釋放由malloc建立的。而且采 用delete釋放後的對象指標,需要obj = NULL以防止“野指標”。
提示:一種情況是,指標變數沒有被初始化,它的指向是隨機的,它會亂指一氣,並不是NULL。如果使用if語句判斷,則認為是有效指標。另一種情況是,指標變數被free或者 delete之後,它們只是把指標所指的記憶體釋放掉,但並沒有把指標本身清除,此時指標指向的就是“垃圾”記憶體。如果使用if語句判斷,也會認為是有效指標。“野指標”是很危險的,良好的編程習慣是,這兩種情況下都需要將指標設定為NULL。這是避免“野指標”的唯一方法。
2:Cocos2d-x記憶體管理
在3.x版本,Cocos2d-x採用全新的根類Ref ,實現Cocos2d-x類對象的引用計數記錄。引擎中的所有類都派生自Ref。Cocos2d-x記憶體管理是建立在C++語言new/delete之上,通過引入Object-C語言的引用計數來實現的。
2-1:記憶體引用計數
Ref類設計來源於Cocos2d-iphone的CCObject 類,在Cocos2d-x 2.x中也叫CCObject類。因此Ref類的記憶體管理是參考Objectives手動管理引用計數(reference count,RC)而設計的。
每個Ref對象都有一個內部計數器,這個計數器跟蹤對象的引用次數,被稱為“引用計數”(RC)。當對象被建立時,引用計數為1。為了保證對象的存在,可以調用retain函數保 持對象,retain會使其引用計數加1,如果不需要這個對象可以調用release函數,release使其引用計數減1。當對象的引用計數為0時,引擎就知道不再需要這個對象了,就會通過delete釋放對象記憶體。
核心類Ref:實現了引用計數。 /** * CCRef.h **/class CC_DLL Ref{ public: void retain(); // 保留。引用計數+1 void release(); // 釋放。引用計數-1 Ref* autorelease(); // 實現自動釋放。 unsigned int getReferenceCount() const; // 被引用次數 protected: Ref(); // 初始化 public: virtual ~Ref(); // 析構 protected: unsigned int _referenceCount; // 引用次數 friend class AutoreleasePool; // 自動釋放池};/** * CCRef.cpp **/// 節點被建立時,引用次數為 1Ref::Ref() : _referenceCount(1){}void Ref::retain(){ CCASSERT(_referenceCount > 0, "reference count should greater than 0"); ++_referenceCount;}void Ref::release(){ CCASSERT(_referenceCount > 0, "reference count should greater than 0"); --_referenceCount; if (_referenceCount == 0) { delete this; }}
Cocos2d-x提供引用計數管理記憶體的方法如下:
- 調用 retain()方法:令其引用計數增1,表示擷取該對象的引用權。
- 調用 release()方法:在引用結束的時候,令其引用計數值減1,表示釋放該對象的引用權。
- 調用 autorelease()方法 :將對象放入自動釋放池。當釋放池自身被釋放的時候,它就會對池中的所有對象執行一次release()方法,實現靈活的記憶體回收。Cocos2d-x提供AutoreleasePool,管理自動釋放對象。當釋放池自身被釋放的時候,它就會對池中的所有對象執行一次release()方法。
Ref原理分析:
- 當一個Ref初始化(被new出來時),_referenceCount = 1;
- 當調用該Ref的retain()方法時,_referenceCount++;
- 當調用該Ref的release()方法時,_referenceCount–;
- 若_referenceCount減後為0,則delete該Ref。
2-2:autorelease使用
當一個繼承自Ref的obj對象建立後,其引用計數_referenceCount為1;執行一次autorelease()後,obj對象被加入到當前的自動釋放池AutoreleasePool,它能夠管理即將釋放的對象池。obj對象的引用計數值並沒有減1。但是在一幀結束時刻或者稱一個訊息迴圈結束時刻,當前的自動釋放池會被回收掉,並對自動釋放池中的所有對象執行一次release()操作,當對象的引用計數為0時,對象會被釋放掉。
所謂所謂一幀或者一個訊息迴圈,即是一個gameloop(在導演類中)。每次為了處理新的事件,Cocos2d-x引擎都會建立一個新的自動釋放池,事件處理完成後,就會銷毀這個池,池中對象的引用計數會減1,如果這個引用計數會減0,也就是沒有被其它類或Ref對象retain,則釋放對象,否則這個對象不會釋放,在這次銷毀池過程中“倖存”下來,它被轉移到下一個池中繼續生存。總結起來就是:
系統在每一幀結束時都會銷毀當前自動釋放池,並建立一個新的自動釋放池;
自動釋放池在銷毀時會對池中的所有對象執行一次release()操作;
新建立的對象如果一幀內不使用,就會被自動釋放;
2-2-1 autorelease源碼如下:
Ref* Ref::autorelease(){ // 將節點加入自動釋放池 PoolManager::getInstance()->getCurrentPool()->addObject(this); return this;}
2-2-2 與autorelease相關的類如下:
(1)AutoreleasePool類: 管理一個 vector 數組來存放加入自動釋放池的對象。提供對釋放池的清空操作。
// 存放釋放池對象的數組std::vector<Ref*> _managedObjectArray;// 往釋放池添加對象void AutoreleasePool::addObject(Ref* object){ _managedObjectArray.push_back(object);}// 清空釋放池,將其中的所有對象都 deletevoid AutoreleasePool::clear(){ // 釋放所有對象 for (const auto &obj : _managedObjectArray) { obj->release(); } // 清空vector數組 _managedObjectArray.clear();}// 查看某個對象是否在釋放池中bool AutoreleasePool::contains(Ref* object) const{ for (const auto& obj : _managedObjectArray) { if (obj == object) return true; } return false;}
(2)PoolManager 類: 管理一個 vector 數組來存放自動釋放池。預設情況下引擎只建立一個自動釋放池,因此這個類是提供給開發人員使用的,例如出於效能考慮添加自己的自動釋放池。
// 釋放池管理器單例對象static PoolManager* s_singleInstance;// 釋放池數組std::vector<AutoreleasePool*> _releasePoolStack;// 擷取 釋放池管理器的單例PoolManager* PoolManager::getInstance(){ if (s_singleInstance == nullptr) { // 建立一個管理器對象 s_singleInstance = new PoolManager(); // 添加一個自動釋放池 new AutoreleasePool("cocos2d autorelease pool");// 內部使用了釋放池管理器的push,這裡的調用很微妙,讀者可以動手看一看 } return s_singleInstance;}// 擷取當前的釋放池AutoreleasePool* PoolManager::getCurrentPool() const{ return _releasePoolStack.back();}// 查看對象是否在某個釋放池內bool PoolManager::isObjectInPools(Ref* obj) const{ for (const auto& pool : _releasePoolStack) { if (pool->contains(obj)) return true; } return false;}// 添加釋放池對象void PoolManager::push(AutoreleasePool *pool){ _releasePoolStack.push_back(pool);}// 釋放池對象出棧void PoolManager::pop(){ CC_ASSERT(!_releasePoolStack.empty()); _releasePoolStack.pop_back();}
我們可以自己建立AutoreleasePool,管理對象的autorelease。
我們已經知道,調用了autorelease()方法的對象(下面簡稱”autorelease對象”),將會在自動釋放池釋放的時候被釋放一次。雖然,Cocos2d-x已經保證每一幀結束後釋放一次釋放池,並在下一幀開始前建立一個新的釋放池,但是我們也應該考慮到釋放池本身維護著一個將要執行釋放操作的對象列表,如果在一幀之內產生了大量的autorelease對象,將會導致釋放池效能下降。因此,在產生autorelease對象密集的地區(通常是迴圈中)的前後,我們最好可以手動建立並釋放一個回收池。
(3)DisplayLinkDirector 類: 這是一個導演類,提供遊戲的主迴圈,實現每一幀的資源釋放。這個類的名字看起來有點怪,但是不用管它。因為這個類繼承了 Director 類,也是唯一一個繼承了 Director 的類,也就是說完全可以合并為一個類,引擎開發人員在源碼中有部分說明。
void DisplayLinkDirector::mainLoop(){ //第一次當導演 if (_purgeDirectorInNextLoop) { _purgeDirectorInNextLoop = false; purgeDirector();//進行清理工作 } else if (! _invalid) { // 繪製情境,遊戲主要工作都在這裡完成 drawScene(); // 清空資源集區 PoolManager::getInstance()->getCurrentPool()->clear(); }}
2-2-3 總結:
autorelease()的實質是將對象加入自動釋放池,對象的引用計數不會立刻減1,在自動釋放池被回收時對象執行release()。
autorelease()只有在自動釋放池被釋放時才會進行一次釋放操作,如果對象釋放的次數超過了應有的次數,則這個錯誤在調用autorelease()時並不會被發現,只有當自動釋放池被釋放時(通常也就是遊戲的每一幀結束時),遊戲才會崩潰。在這種情況下,定位錯誤就變得十分困難了。例如,在遊戲中,一個對象含有1個引用計數,但是卻被調用了兩次autorelease()。在第二次調用autorelease()時,遊戲會繼續執行這一幀,結束遊戲時才會崩潰,很難及時找到出錯的地點。因此,我們建議在開發過程中應該避免濫用autorelease(),只在Factory 方法等不得不用的情況下使用,盡量以release()來釋放對象引用。
autorelease()並不是毫無代價的,其背後的釋放池機制同樣需要佔用記憶體和CPU資源。過多的使用autorelease()會增加自動釋放池的管理和釋放池維護對象存取釋放的支出。在記憶體和CPU資源本就不足的程式中使得系統資源更加緊張。此時就需要我們合理建立自動釋放池管理對象autorelease。
不用的對象推薦使用release()來釋放對象引用,立即回收。
3:Ref特殊記憶體管理
(1)Node的addChild/removeChild方法
在Cocos2d-x中,所有繼承自Node類,在調用addChild方法添加子節點時,子節點會自動調用了retain。 對應的通過removeChild,移除子節點時,子節點會自動調用了release。
調用addChild方法添加子節點,節點對象執行retain。子節點被加入到節點容器中,父節點銷毀時,會銷毀節點容器釋放子節點,即對子節點執行release。如果想提前移除子節點我們可以調用removeChild。
在Cocos2d-x記憶體管理中,大部分情況下我們通過調用addChild/removeChild的方式自動完成了retain,release調用。不需再調用retain,release。
(2)樹形結構和鏈式反應
我們當前運行這一個情境,情境初始化,添加了很多層,層裡面有其它的層或者精靈,而這些都是CCNode節點,以情境為根,形成一個樹形結構,情境初始化之後(一幀之後),這些節點將完全 依附(內部通過retain)在這個樹形結構之上,全權交由樹來管理,當我們砍去一個樹枝,或者將樹連根拔起,那麼在它之上的“子節點”也會跟著去除(內部通過release),這便是鏈式反應。
(3)Factory 方法
在Cocos2d-x中,提供了大量的Factory 方法create靜態函數建立對象。仔細看你會發現,這些對象都是自動釋放的。
#define CREATE_FUNC(__TYPE__) static __TYPE__* create() { __TYPE__ *pRet = new __TYPE__(); if (pRet && pRet->init()) { pRet->autorelease(); return pRet; } else { delete pRet; pRet = NULL; return NULL; } }
下面以一段代碼說明Cocos2d-x對象自動釋放過程。
void HelloWorld::test(){ auto lable = Lable::create(); this->addChild(lable);}
- 首先通過create方法建立了一個lable(建立過程中執行lable->autorelease()),對象被加入到當前的自動釋放池AutoreleasePool中,此時label的引用計數為1;
- 執行this->addChild(lable)將label作為子節點加入Layer父節點中,此時label的引用計數為2;
- 當一幀結束後,當前的自動釋放池AutoreleasePool被釋放,對label執行了一次release操作,label的引用計數變為1,當前的自動釋放池被銷毀,lable->autorelease()的作用結束;
- 當遊戲結束或者是切換情境時,label的父節點Layer會被銷毀,發生鏈式反應,Layer對其子節點label執行release操作,label的引用計數變為0,執行delete label操作,記憶體得以回收。
4:記憶體管理使用建議
(1)類內部使用new分配的記憶體,在解構函式中執行delete操作;
(2)cocos2d-x的類元素通過create靜態工廠建立,其記憶體會通過autorelease自動管理,無需其他動作;
(3)每個retain函數一定要對應一個release函數或一個autorelease函數;
(4)單例模式下由於只有一個執行個體,可以寫一個成員函數用於delete對象指標,在遊戲結束時執行該成員函數就可以了;
Cocos2d-x學習筆記—記憶體管理機制