cocos2dx基於引用計數管理內存,所有繼承自CCObject
的對象都將獲得引用計數的能力,可通過調用retain
成員函數用於引用計數值,調用release
減少引用計數值,當計數值減為0時銷毀對象.
cocos2dx的對象管理是樹形結構的,可通過調用父親節點的addChild
成員函數將一個子節點對象添加到父節點中,當子節點被添加到父親節點中,子節點的引用計數值加1,如果通過removeChild
將子節點從父節點中移除子節點的引用計數值減1.當父節點被銷毀時,會遍歷其所有的子節點,將其子節點的引用計數值減1,當子節點的計數值為0,銷毀子節點.
提到cocos2dx的內存管理,就不得不提autorelease
.
我們隨便查看一個函數:
CCSprite* CCSprite::createWithTexture(CCTexture2D *pTexture) { CCSprite *pobSprite = new CCSprite(); if (pobSprite && pobSprite->initWithTexture(pTexture)) { pobSprite->autorelease(); return pobSprite; } CC_SAFE_DELETE(pobSprite); return NULL; }
注意到CCSprite
被生成並成功初始化之后(initWithTexture
)立刻就調用了autorelease
函數.那么autorelease
的作用到底是什么.首先當一個對象A被創建出來之后它的引用計數值必定為1,如果我們將A添加到一個父節點B中,此時它的引用計數值為2.之后銷毀父節點B,依據上面的描述,父節點被銷毀時會將其子節點的引用計數減1.也就是說父節點B被銷毀之后A的引用計數值為1。那么我們就必須手動在不再需要A的時候調用release
或delete
來將A銷毀.為了減輕程序員的負擔,cocos2dx提供了CCAutoreleasePool
當節點A被添加到autoreleasepool
之后,我們就不在需要關注A的銷毀了.因為引擎會在每幀結束之后清理autoreleasepool
中的對象,調用它的release
如果如果對象引用計數值降為0將對象銷毀.
下面是CCAutoreleasePool::addObject
,autorelease
最終調用的就是這個函數:
void CCAutoreleasePool::addObject(CCObject* pObject) { m_pManagedObjectArray->addObject(pObject); CCAssert(pObject->m_uReference > 1, "reference count should be greater than 1"); ++(pObject->m_uAutoReleaseCount); pObject->release(); // no ref count, in this case autorelease pool added. }
向CCAutoreleasePool
中添加一個對象,底層管理容器是一個array
對象被添加到array
的時候不檢測重復性,所以個一對象可以被添加多次.添加完成后需要調用release
函數減少引用計數,因為在array->addObject
中調用了retain
.
應當注意避免對一個object
多次調用autorelease
,對object
調用一次autorelease
將導致當前幀結束時對這個對象調用一次release
,假設一個對象沒被添加進任何容器,則其引用計數值為1,如果調用兩次autorelease
則當前幀結束時會調用兩次release
其中第二次必定是在一個已經被銷毀的對象上執行的.而如果這一對象已經添加另外一個容器,則會導致那個容器在當前幀結束之后持有一個已經被銷毀的對象指針.此問題在cocos2dx rc3中已經添加了assert處理,對象銷毀時判斷在pool
是否有另外的實例存在,如果有則報錯.雖然只在debug版本中處理,但也不用通過遍歷vector
這么低效的手段來判斷重復實例的存在吧.難道2.2.3版本的m_uAutoReleaseCount
就沒有給開發人員提供一點啟發?
我們來看看引擎的主循環:
void CCDisplayLinkDirector::mainLoop(void) { if (m_bPurgeDirecotorInNextLoop) { m_bPurgeDirecotorInNextLoop = false; purgeDirector(); } else if (! m_bInvalid) { drawScene(); // release the objects CCPoolManager::sharedPoolManager()->pop(); } }
在每幀中調用drawScene()
,之后就調用CCPoolManager::sharedPoolManager()->pop()
.
void CCPoolManager::pop() { if (! m_pCurReleasePool) { return; } int nCount = m_pReleasePoolStack->count(); m_pCurReleasePool->clear(); if(nCount > 1) { m_pReleasePoolStack->removeObjectAtIndex(nCount-1); // if(nCount > 1) // { // m_pCurReleasePool = m_pReleasePoolStack->objectAtIndex(nCount - 2); // return; // } m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount - 2); } /*m_pCurReleasePool = NULL;*/ }
好了關鍵函數就是m_pCurReleasePool->clear()
,這個函數完成了對象的清理過程.
在這里我們發現了一點奇怪的地方,為啥有個m_pReleasePoolStack
還有個m_pCurReleasePool
.
首先來看下ReleasePool
對象是在哪里被創建的:
void CCPoolManager::push() { //if(!m_pCurReleasePool){ CCAutoreleasePool* pPool = new CCAutoreleasePool(); //ref = 1 m_pCurReleasePool = pPool; m_pReleasePoolStack->addObject(pPool); //ref = 2 pPool->release(); //ref = 1 //} }
然后我們跟蹤程序的運行將斷點放在push
函數里,斷點的第一次命中調用棧如下:
libcocos2d.dll!cocos2d::CCPoolManager::push() 行 145 C++ libcocos2d.dll!cocos2d::CCPoolManager::getCurReleasePool() 行 200 C++ libcocos2d.dll!cocos2d::CCPoolManager::addObject(cocos2d::CCObject * pObject=0x003e9938) 行 189 C++ libcocos2d.dll!cocos2d::CCObject::autorelease() 行 100 C++ libcocos2d.dll!cocos2d::CCDictionary::create() 行 396 C++ libcocos2d.dll!cocos2d::CCConfiguration::init() 行 60 C++ libcocos2d.dll!cocos2d::CCConfiguration::sharedConfiguration() 行 154 C++ libcocos2d.dll!cocos2d::CCDirector::setDefaultValues() 行 199 C++ libcocos2d.dll!cocos2d::CCDirector::init() 行 113 C++ libcocos2d.dll!cocos2d::CCDirector::sharedDirector() 行 97 C++ IslandFight.exe!AppDelegate::applicationWillEnterForeground() 行 143 C++ libcocos2d.dll!cocos2d::CCEGLView::WindowProc(unsigned int message=5, unsigned int wParam=0, long lParam=20972000) 行 444 C++ libcocos2d.dll!cocos2d::_WindowProc(HWND__ * hWnd=0x002a06aa, unsigned int uMsg=5, unsigned int wParam=0, long lParam=20972000) 行 171 C++
第二次調用棧:
libcocos2d.dll!cocos2d::CCPoolManager::push() 行 145 C++ libcocos2d.dll!cocos2d::CCDirector::init() 行 165 C++ libcocos2d.dll!cocos2d::CCDirector::sharedDirector() 行 97 C++ IslandFight.exe!AppDelegate::applicationWillEnterForeground() 行 143 C++ libcocos2d.dll!cocos2d::CCEGLView::WindowProc(unsigned int message=5, unsigned int wParam=0, long lParam=20972000) 行 444 C++ libcocos2d.dll!cocos2d::_WindowProc(HWND__ * hWnd=0x002a06aa, unsigned int uMsg=5, unsigned int wParam=0, long lParam=20972000) 行 171 C++
也就是說總共創建了兩個ReleasePool
,ReleasePool
都被添加到m_pReleasePoolStack
成員中,這個成員是一個array
而m_pCurReleasePool
則指向m_pReleasePoolStack
的最后一個成員.
接着我們將斷點放到CCPoolManager::pop()
中,可以發現第一次pop
命中的時候nCount == 2
,所以執行if(nCount > 1)
之后的流程, 這個流程的處理就是將m_pReleasePoolStack
的最后一個成員銷毀,然后將m_pCurReleasePool
指向m_pReleasePoolStack
中最后一個成員.
繼續觀察程序的運行可以發現,在第一幀之后m_pReleasePoolStack
中實際上永遠只剩一個成員.我將
bool CCDirector::init(void)
中的CCPoolManager::sharedPoolManager()->push()
注釋掉,程序依舊能正確的運行.
我搜索cocos2dx 2.2.3和cocos2dx 3.0rc源代碼,並無找到必須啟用多個ReleasePool
的地方, 那么多ReleasePool
存在的意義就僅剩調試了.
研究cocos2dx不久,文中有理解不當的地方歡迎指正.