目標讀者:瞭解 Cocos2d-x 中的節點以及節點樹,瞭解引用計數,瞭解遊戲主循環等概念。html
本文首先介紹 Cocos2d-x 3.2 中內存管理的做用,以及各個做用的應用。藉由通俗易懂的解釋來了解內存管理的過程。其次經過源碼解析介紹其內部的實現原理。加深理解,從而在有須要的時候繞開引擎創建本身的內存管理機制。node
1) 經過加入 autorelease
來自動釋放那些建立後未使用的對象。
2) 經過節點管理來保證對象在棄用後及時地刪除。git
使用條件:該對象是Node的子類對象
使用方法:addChild、removeChildgithub
內存管理過程:數組
addChild //添加對象後,對象能夠被使用 removeChild //刪除對象後,對象被馬上刪除(經過 delete)
簡述:新建立的對象若是一幀內不使用,就會被自動釋放。(所謂一幀,便是一個gameloop。)
使用條件:對象經過CREAT_FUNC()
宏建立或者對象使用autorelease
加入了自動釋放池。
使用方法:自動實現函數
內存管理過程:oop
對象建立 引用+1 對象自動釋放 引用-1
對象建立 引用+1 對象使用 引用+1 // 經過 addChild 使用對象 對象自動釋放 引用-1
引用的初始值爲0,若是一幀結束後對象的引用值仍是0,那就就會被 delete。性能
涉及內存管理的文件不少,僅展現直接相關的部分代碼。this
Ref
類: 進行引用計數、提供加入自動釋放池的接口。lua
AutoreleasePool
類: 管理一個 vector
數組來存放加入自動釋放池的對象。提供對釋放池的清空操做。
PoolManager
類: 管理一個 vector
數組來存放自動釋放池。默認狀況下引擎只建立一個自動釋放池,所以這個類是提供給開發者使用的,例如出於性能考慮添加本身的自動釋放池。
DisplayLinkDirector
類: 這是一個導演類,提供遊戲的主循環,實現每一幀的資源釋放。這個類的名字看起來有點怪,可是不用管它。由於這個類繼承了 Director
類,也是惟一一個繼承了 Director
的類,也就是說徹底能夠合併爲一個類,引擎開發者在源碼中有部分說明。
// 引用計數變量 unsigned int _referenceCount; // 對象被構造後,引用計數值爲 1 Ref::Ref() : _referenceCount(1) //當Ref對象被建立時,引用計數的值爲 1 { #if CC_ENABLE_SCRIPT_BINDING static unsigned int uObjectCount = 0; _luaID = 0; _ID = ++uObjectCount; _scriptObject = nullptr; #endif #if CC_USE_MEM_LEAK_DETECTION trackRef(this); #endif } // 引用+1 void Ref::retain() { CCASSERT(_referenceCount > 0, "reference count should greater than 0"); ++_referenceCount; } // 引用-1 。若是引用爲0則釋放對象 void Ref::release() { CCASSERT(_referenceCount > 0, "reference count should greater than 0"); --_referenceCount; if (_referenceCount == 0) { #if CC_USE_MEM_LEAK_DETECTION untrackRef(this); #endif delete this; // 注意這裏 把對象 delete 了 } } // 提供加入自動釋放池的接口。對象調用此函數便可加入自動釋放池的管理。 Ref* Ref::autorelease() { PoolManager::getInstance()->getCurrentPool()->addObject(this); return this; } //獲取引用計數值 unsigned int Ref::getReferenceCount() const { return _referenceCount; }
// 存放釋放池對象的數組 std::vector<Ref*> _managedObjectArray; // 往釋放池添加對象 void AutoreleasePool::addObject(Ref* object) { _managedObjectArray.push_back(object); } // 清空釋放池,將其中的全部對象都 delete void 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; }
// 釋放池管理器單例對象 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(); }
void DisplayLinkDirector::mainLoop() { //第一次當導演 if (_purgeDirectorInNextLoop) { _purgeDirectorInNextLoop = false; purgeDirector();//進行清理工做 } else if (! _invalid) { // 繪製場景,遊戲主要工做都在這裏完成 drawScene(); // 清空資源池 PoolManager::getInstance()->getCurrentPool()->clear(); } }
根據目前的分析,咱們先來捋一捋,待會兒再進一步深刻。內存管理的過程是怎麼樣的呢?首先,建立了一個 Node
對象A,Node
繼承Ref,所以 Ref
的引用計數爲1;而後,A經過 autorelease
將本身放入自動釋放池;drawScene()
完成後,一幀結束,Director
經過釋放池將池中的對象 clear()
,即對 Node
對象A進行 release()
操做。A的引用計數變爲0,執行 delete
釋放A對象。
接下來咱們繼續介紹另外幾個與內存管理有關的類。
Node
類:提供了 addChild
和 removeChild
方法來建立遊戲的節點樹。
Vector
類:封裝了對於對象的 retain
操做和 release
操做。
// 添加節點 void Node::addChild(Node *child) { CCASSERT( child != nullptr, "Argument must be non-nil"); this->addChild(child, child->_localZOrder, child->_name); // 通過這個方法-->addChildHelper-->insertChild,完成retain操做 } // 移除節點 void Node::removeChild(Node* child, bool cleanup /* = true */) { // if (_children.empty()) { return; } // ssize_t index = _children.getIndex(child); if( index != CC_INVALID_INDEX ) this->detachChild( child, index, cleanup );//注意這個函數 } // 插入節點 void Node::insertChild(Node* child, int z) { _transformUpdated = true; _reorderChildDirty = true; _children.pushBack(child);// pushBack方法對節點進行了retain child->_setLocalZOrder(z); } // 剝離節點 void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup) { ...// 部分省略 _children.erase(childIndex);// erase方法對節點進行了release }
// 這裏僅展現與Node類相關的內存管理的部分 // 將對象入棧,引用+1 void pushBack(T object) { CCASSERT(object != nullptr, "The object should not be nullptr"); _data.push_back( object ); object->retain(); // 進行了retain } // 將目標位置的對象移除 iterator erase(ssize_t index) { CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!"); auto it = std::next( begin(), index ); (*it)->release(); // 進行了release return _data.erase(it); }
到這裏,終於能夠把故事完整地講一遍了。內存管理的過程是怎麼樣的呢?首先,建立了一個 Node
對象A, Node
繼承 Ref
,所以 Ref
的引用計數爲1;而後A又經過 autorelease
將本身放入自動釋放池;接着,有個 Node
對象B,B經過 addChild(A)
使得A的引用+1;幾個 mainLoop
後,B經過 removeChild(A)
使得A的引用-1;這個 mainLoop
的 drawScene()
完成後,一幀結束, Director
經過釋放池將池中的對象 clear()
,即對 Node
對象A進行 release
操做。A的引用計數變爲0,執行 delete
釋放A對象。
之因此稱爲高階用法,是由於,若是開發者對 Cocos 的內存管理機制理解不夠深入,那麼極可能會用錯而致使損失大於收益。另外一方面,這類用法在平時不多會用到。
retain
來延長對象的生存時間在開發過程當中,若是須要使用一個節點對象,可是又不想把它放到節點樹裏面去。那麼就可使用 retain
來避免對象被自動釋放掉。
PoolManager
的 push
來延長對象的生存時間有些狀況下,但願閒置對象晚一幀進行銷燬,可使用 push
把當前釋放池推入棧底,那麼這一幀結束的時候只會釋放剛 push
進去的釋放池。
筆者自己尚未機會使用太高階用法,若是有小夥伴發現了高階用法在實際問題中的應用,敬請留言交流。
深刻理解 Cocos2d-x 內存管理 (從這篇文章中得到許多啓發。)
Cocos2d-x源碼(位於Github上,若是連不上嘗試使用前綴 https)
引用計數——維基百科(關於引用計數的說明)
Cocos2d-x的內存管理機制概述(裏面提到了爲何要有 PoolManager)
cocos2dx 3.2 (24)——內存管理機制(編纂地比較詳細)