不過在這以前,咱們要先引入另外一個類.Cocos2d-x的核心類之一CCDirector.這個類能夠說是引擎最主要的類了,沒有他,引擎內的全部東西都沒法運轉起來.因爲這個類有點大,作的事情不少,我就不一一粘出來看了,咱們只關心跟內存管理有關的函數.
由於一部片子只須要一個導演,因此CCDirector也就是一個單例類,是多重繼承自CCObject和TypeInfo,有什麼用,暫時無論.不過是採用的二段構建的單例.什麼是二段構建,說簡單點,就是不在構造函數中作初始化工做,而在另外一個函數裏作初始化工做.這樣作有什麼好處?能夠說沒有好處,也能夠說有好處.的看用的人是出於一種什麼樣的目的了.好比說咱們這裏,若是不用二段構建是不可能實現的.由於CCDirector是個抽象類,咱們知道,抽象類是不能被實例化的,也就是說,你new XXX是不可能的,編譯器直接彈錯.因此要想子類擁有父類的初始化功能,那隻能另外寫一個了.
那咱們是如何構建的CCDirector這個單例的呢?
static CCDisplayLinkDirector *s_SharedDirector = NULL;
CCDirector* CCDirector::sharedDirector(void)
{
if (!s_SharedDirector)
{
s_SharedDirector = new CCDisplayLinkDirector();
s_SharedDirector->init();
}
return s_SharedDirector;
}
看見了吧,他實際上是new了一個本身的子類,來完成本身的功能的.而這個子類裏,就有咱們很是重要的一個函數.
在CCDirector的初始化函數裏,咱們看到了一個熟悉的面孔
bool CCDirector::init(void)
{
CCLOG("cocos2d: %s", cocos2dVersion());
.........................................
// create autorelease pool
CCPoolManager::sharedPoolManager()->push();
return true;
}
他在整個內核開始運行以前,就初始化了一個內存管理類.以後,在他的析構函數裏
CCDirector::~CCDirector(void)
{
CCLOG("cocos2d: deallocing CCDirector %p", this);
.....................
// pop the autorelease pool
CCPoolManager::sharedPoolManager()->pop();
CCPoolManager::purgePoolManager();
...........................
}
用管理類執行了一個彈棧操做pop().不過看到這裏,我有點不解,pop是彈出當前管理池並clear掉,那若是當前有幾個管理池同時存在呢?只彈一次,後面幾個怎麼辦?咱們仍是慢慢來吧.
purgePoolManager()實際上是這樣
void CCPoolManager::purgePoolManager()
{
CC_SAFE_DELETE(s_pPoolManager);
}
他刪掉當前單例的指針.這樣整個單例所保存的數據都會被刪除掉,因此也就不用pop全部的元素了.
而後這個CCDirector 中剩下的惟一跟內存有關的,也是最最重要的函數mainLoop(),從他的名字咱們就能看出來他的重要性了--主循環.他是一個純虛函數virtual void mainLoop(void) = 0,在上面提到的子類CCDisplayLinkDirector中被覆寫了.
如今咱們來看看這個類
class CCDisplayLinkDirector : public CCDirector
{
public:
CCDisplayLinkDirector(void)
: m_bInvalid(false)
{}
virtual void mainLoop(void);
virtual void setAnimationInterval(double dValue);
virtual void startAnimation(void);
virtual void stopAnimation();
protected:
bool m_bInvalid;
};
他其實就是覆寫了抽象類的幾個純虛函數而已.而且經過註釋,咱們知道他還有些其餘的功能和限制.
1.他負責顯示並以必定的頻率刷新計時器.
2.計時器和界面繪製都是經過必定的頻率是同步進行的.
3.只支持每秒60,30,15幀設置.
這些不屬於咱們討論範圍,簡答提一下,咱們只關心重要的mainLoop()
void CCDisplayLinkDirector::mainLoop(void)
{
if (m_bPurgeDirecotorInNextLoop)
{
m_bPurgeDirecotorInNextLoop = false;
purgeDirector();
}
else if (! m_bInvalid)
{
drawScene();
// release the objects
CCPoolManager::sharedPoolManager()->pop();
}
}
這裏在沒有失效的情況下(即m_bInvalid不爲真),他會執行管理池中的pop函數.至於什麼時候m_bInvalid爲真,實際上是在這裏
void CCDisplayLinkDirector::stopAnimation(void)
{
m_bInvalid = true;
}
而上面的條件語句中的這個變量m_bPurgeDirecotorInNextLoop,咱們從名字裏就能看出來,他是不是結束CCDirector的一個標誌.既然是mainLoop,那就必定要Loop起來,而這裏並無看到任何Loop的跡象.因而我在內核中查找一下mainLoop在哪裏被用過.
int CCApplication::run()
{
PVRFrameEnableControlWindow(false);
// Main message loop:
MSG msg;
LARGE_INTEGER nFreq;
LARGE_INTEGER nLast;
LARGE_INTEGER nNow;
QueryPerformanceFrequency(&nFreq);
QueryPerformanceCounter(&nLast);
// Initialize instance and cocos2d.
if (!applicationDidFinishLaunching())
{
return 0;
}
CCEGLView* pMainWnd = CCEGLView::sharedOpenGLView();
pMainWnd->centerWindow();
ShowWindow(pMainWnd->getHWnd(), SW_SHOW);
while (1)
{
if (! PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
// Get current time tick.
QueryPerformanceCounter(&nNow);
// If it's the time to draw next frame, draw it, else sleep awhile.
if (nNow.QuadPart - nLast.QuadPart >m_nAnimationInterval.QuadPart)
{
nLast.QuadPart = nNow.QuadPart;
CCDirector::sharedDirector()->mainLoop();
}
else
{
Sleep(0);
}
continue;
}
if (WM_QUIT == msg.message)
{
// Quit message loop.
break;
}
// Deal with windows message.
if (! m_hAccelTable || ! TranslateAccelerator(msg.hwnd,m_hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
如此大的一個while(1),就是在這裏循環的.這個run又是在哪裏運行的呢?你們看工程裏的main.cpp
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// create the application instance
AppDelegate app;
CCEGLView* eglView = CCEGLView::sharedOpenGLView();
eglView->setFrameSize(960, 640);
returnCCApplication::sharedApplication()->run();
}
在這裏,WIN32平臺下的入口函數中,咱們的引擎就已經啓動了.其餘的功能,是啓動的一些初始化工做,以及跨平臺的東西,這裏不在討論範圍以內,咱們只管內存管理的東西.
好,基本的過程我都找到了,如今來理一下自動釋放的思路.
假設咱們程序已經運行,而且已經存進指針了.那麼mainLoop這個函數,在不受到阻礙的狀況下,會一直執行,而且一直執行CCPoolManager::sharedPoolManager()->pop().這裏咱們再把這個pop搬出來看看.還有一個附帶的clear().
void CCPoolManager::pop()
{
if (! m_pCurReleasePool)
{
return;
}
int nCount = m_pReleasePoolStack->count();
m_pCurReleasePool->clear();
if(nCount > 1)
{
m_pReleasePoolStack->removeObjectAtIndex(nCount-1);
m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount- 2);
}
}
void CCAutoreleasePool::clear()
{
if(m_pManagedObjectArray->count() > 0)
{
//CCAutoreleasePool* pReleasePool;
CCObject* pObj = NULL;
CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
{
if(!pObj)
break;
--(pObj->m_uAutoReleaseCount);
}
m_pManagedObjectArray->removeAllObjects();
}
}
1.若是沒有有釋放池,就不作任何事,咱們有池,一開始就push了一個進去.
2.記錄當前池中的指針個數,假設咱們有3個.在棧中的順序以及引用計數分別爲4(最後一針),2(第二針),1(第一針);
3.清除掉當前池中的指針.釋放的過程是,遍歷池中每個指針,將他們的自動釋放計數減一,而後removeAllObject.看看removeAllObjects()幹了些什麼事吧.
void CCArray::removeAllObjects()
{
ccArrayRemoveAllObjects(data);
}
額,他其實只是調用了一下ccArray的函數而已,而裏面的參數data,就是ccArray結構體的指針,也就是咱們當前的自動釋放池.
/** Removes all objects from arr */
void ccArrayRemoveAllObjects(ccArray *arr)
{
while( arr->num > 0 )
{
(arr->arr[--arr->num])->release();
}
}
這個函數寫着,從數組裏移除全部的指針.其實不算是移除,不算是真正的移除,他只是遍歷了一次儲存指針的內存地址(即儲存內存地址的內存塊),並分別執行了一次release操做,即將咱們指針的引用計數減1,若是減一以後爲0了,那麼就刪除(delete)這個指針.web
我來模擬一下他的運行過程:
1.找到第一個引用計數爲4的指針(最後一針),將他的釋放計數減1,變爲0;而後找到第二針,釋放計數減1,變爲0;最後找到第一針,釋放計數減1,變爲0.
2.執行removeAll操做,將棧中每個指針的引用計數減1.找到最後一針,4->3,第二針2->1,第一針1->0,而後第一針執行delete this操做.即馬上執行析構函數.
CCObject::~CCObject(void)
{
// if the object is managed, we should remove it
// from pool manager
if (m_uAutoReleaseCount > 0)
{
CCPoolManager::sharedPoolManager()->removeObject(this);
}
// if the object is referenced by Lua engine, remove it
.....................
}
不過由於m_uAutoReleaseCount已經變成0了,就等於什麼都不作,只是例行C++內核過程.那前面一篇提到的m_uAutoReleaseCount何時會大一1,這個通過個人研究發現,他不能大於1,他只有0和1兩個值,你把它定成bool類型的也能夠,只是不方便運算.windows
呵呵,重點來了.別覺得delete幹了件跟牛X的事,其實不少初學者都被他的名字騙了.delete其實只幹了一件事,那就是把指針指向的內存中的數據給刪了,可是指針並無消失.他任然存在.這就是傳說中的野指針,瞎指!,若是使用它,可能就會出現莫名其妙的BUG.因此,通常delete後,都要將指針的賦值爲NULL,讓系統知道,這指針暫時沒用了.不過這裏是delete this,this這個指針是極日常又不日常的一個東西,每一個類生成的時候,都會自動帶一個this指針.this指向的其實就是這個類本身,那delete this就等於"自殺".C++中,容許一個類經過成員函數申請"自殺操做",不過你必須保證,這個類是new出來的,不是對象,並且"自殺"是而且必須是這個類執行的最後一個操做,以後不在對這個類作任何有關數據,以及檢查指針等操做.(想對C++有更深的瞭解,你們能夠去看侯捷先生的<<深度探索C++對象模型>>)那你說,我把delete this放在析構裏就好了.NO,這是絕對不行的,delete this以後的第一個操做就是執行析構函數,若是再放進去,會造成代碼無限循環,致使堆棧溢出.數組
這樣看來,咱們保存指針數組中,任然有3個指針存在,雖然有個已經廢了........
3.mainLoop再運行一次,而後pop在次執行.不過看到這裏我出現了疑問,由於前一次的pop操做,僅僅只是釋放了指針所指向的內存,但並無把指針賦值爲NULL,因此,若是再次操做指針,豈不是變成操做野指針了!來吧,再看看代碼.
經過把輸出數據打印到控制檯,我發現了一個現象.
CCPoolManager::pop 1
CCAutoreleasePool::clear 4
0
1
2
3
..................
CCPoolManager::pop 1
CCAutoreleasePool::clear 0
也就是說,在pop一次以後,當前釋放池中所儲存的指針所有被移除了,不是刪除指針,只是從數組中把它們去除.app
注意,如下爲我我的分析過程當中很是重要的一段!!!函數
這裏我要重要提示一下,這裏卡了我好久好久,也廢了我不少的時間!你們還記得那個自動釋放的計數麼?就是這玩意m_uAutoReleaseCount,他在add中被加了1,在clear中被減一.開始個人想法是,這個值只要是1,就說明他被加入了自動管理池中管理,爲0表示沒有加入自動管理池.可是我發現我錯了,我找遍了整個內核,都沒有找到有哪一個地方在判斷這個值是否爲1(除了CCObject的析構,不過那裏沒有實際意義).oop
也就是說,按照我推理的思路,在pop中清理一次指針後,你得把指針移除吧,因此我想到了removeXX函數,但是他們一次也沒有被執行.可是,上面的信息中卻顯示了釋放池中沒有元素了.那是怎麼釋放的呢?我這時想到了那個釋放計數,他們不是在clear中被歸零了麼,應該有個判斷,找到歸零的指針就刪除,惋惜我錯了,我愣是沒找到這樣的或相似的方法.那這數組到底是如何變成0的呢.ui
其實祕密在這裏.還記得上面自動釋放池執行的那個函數麼?在clear中的removeAllObjects,他最終執行的函數是ccArrayRemoveAllObjects,而這個函數乾的事咱們都知道,那就是this
while( arr->num > 0 )
{
(arr->arr[--arr->num])->release();
}spa
等一下,這裏有個很是陰險的地方,不注意看徹底看不出來,我就是沒注意,就是這個東西!!!!--arr->num!!!!.最開始我僅僅認爲這是循環遍歷數組執行release操做.天哪,當我分析了一遍又一遍時,才發現,這就是讓自動釋放池數組變成0的緣由所在!操作系統
內核做者並無真正的把數組釋放掉,而是強行把數組的元素個數歸零了!!!!!
這句判斷if(m_pManagedObjectArray->count()> 0),其中的count()也就是得到數組元素個數的函數,他的原型是這樣的.
unsigned int CCArray::count()
{
return data->num;
}
其中的data就是一個指向ccArray的指針,而ccArray結構體中的第一個參數就是num.
各位確定還記得我說過的,只delete指針不賦值NULL,是沒辦法真正刪除一個指針的,而他會成爲野指針.做者僅僅執行了delete this,沒有賦值NULL,clear中卻還繼續對野指針進行操做,可是整個引擎卻沒有出現絲毫的BUG.也就是這個緣由,讓我糾結了好久,以致於發現瞭如此坑爹的刪除數組方式.
這裏給你們介紹一下m_uAutoReleaseCount這個自動釋放計數的前身.Cocos2d-x這個引擎實際上是來源於Cocos2d,而這個引擎是蘋果機專用,也就是用的Object-C,而帶引用計數的指針是Object-C語言層面就支持的東西,在蘋果裏面,這個釋放計數實際上是布爾值. .而C++語言層面並不支持釋放計數,也就是帶引用計數的指針,因此只能模擬.這下好了,一模擬就模擬了個我目前爲止以爲是不只廢材還費腦子的變量m_uAutoReleaseCount.我曾經試圖控制他的值,不過引擎會報錯.可是我實在是沒找到有哪裏在判斷他的值了.除了那個可有可無的~CCObject.求高手解答吧!
也就是說,按照內核的自動管理措施,確實能夠釋放掉不用的內存,可是,會生成一大堆的野指針.真是不負責任的自動管理.不過經過我簡單的研究,這個自動管理類,確實沒辦法將野指針消除,只能讓系統回收,算是他引擎的缺陷吧.要否則,Cocos2d的開發者也不會叫咱們儘可能不要用自動釋放.
好啦,重要的分析結束啦!
下面呢,我就把整個自動管理的過程串起來,給你們一個清晰的思路了.
咱們首先new一個類出來 CCSprite *pSprite = CCSprite::create(),這個靜態工廠方法裏,直接就執行了autorelease操做了.
,這裏面的過程就是這樣的:new操做將引用計數初始化爲1,釋放計數初始化爲0, autorelease經過幾回操做,將咱們的pSprite,也就是他的內存地址存放進了自動釋放池裏.而後retain將引用計數加1.這時引用計數爲2.這時,咱們就能夠理解,爲何後面有一個pObject->release()操做了.咱們只是想把他加入自動管理,但並不想retain他.因而乎,引用計數仍是1.
這時,咱們執行addChild(pSprite),將咱們的精靈加入節點中,這個操做也會將引用計數加1.來給你們看看代碼.
……………………..
,我只挑重要的函數講,這裏執行了一個insertChild操做.
void CCNode::insertChild(CCNode* child, int z)
{
m_bReorderChildDirty = true;
ccArrayAppendObjectWithResize(m_pChildren->data, child);
child->_setZOrder(z);
}
,咱們上一篇才提到的函數ccArrayAppendObjectWithResize又出現了,他的出現,就意味着retain的出現.我就不繼續粘代碼了,那這裏的m_pChildren就必定是一個CCArray的指針了.這時pSprite的引用計數爲2.
假設這時咱們的遊戲中有且僅有一個精靈,也就是咱們的pSprite,咱們暫時把他當作是玩家,遊戲中,玩家是咱們主要操做的對象,加入自動釋放後,就有被自動釋放的可能,可是這是咱們所不容許的.引擎天然也知道則一點.因此,這時候mainLoop執行一次,pop執行一次,咱們當前有自動釋放池,因此clear執行一次.clear中,釋放計數被減1,歸零,而後因爲removeAllObjects的執行,咱們的玩家pSprite執行一次release,引用計數減由2變成1.而後數組被強行歸零.這時mainLoop再次執行的話,釋放池中的元素個數就爲0了(沒有再添加其餘的東西).
,這不是坑爹嗎?釋放池就是用來釋放指針的,可是pSprite的引用計數還有1,是不會執行delete this操做的.你說對了,這就是爲何,咱們還能操做玩家的緣由,若是這個指針被釋放了,咱們豈不是在操做野指針了?那如何控制玩家?
其實說到這裏,你們應該明白了.這個自動釋放池,作的事情,僅僅是釋放掉引用計數爲1的指針.只要引用計數超過1,那就不會釋放.作的只是將引用計數減一的操做.那咱們結束遊戲,或者切換場景時,那些引用計數大於1的指針,改如何釋放呢? 分析到這裏的,我我的也認爲,這個內存管理算是比較坑的了.
仔細想一下,內存泄露的緣由是什麼,說簡單點,就是隻new而不delete.什麼狀況下會發生這樣的狀況呢?好比說,咱們的一個射擊遊戲,要發射不少子彈,因而咱們在一個界面裏new不少子彈出來,可是子彈碰撞後不delete掉,以致於子彈愈來愈多,內存越佔越大.而後咱們忽然切換場景Scene,由於場景的對象消亡了,因此場景的析構函數被執行,自動釋放了他佔的內存,可是咱們的子彈並無執行析構函數,因此一直把空間佔着,那段內存就廢掉了,因而乎內存泄露.
可是咱們切換界面的時候,玩家的引用計數爲1,而且再也不釋放池內了,那該如何釋放?這裏,咱們就要看下CCNode的析構函數了.
CCNode::~CCNode(void)
{
CCLOGINFO("cocos2d: deallocing" );
……………………………
if(m_pChildren&& m_pChildren->count() > 0)
{
CCObject*child;
CCARRAY_FOREACH(m_pChildren, child)
{
CCNode*pChild = (CCNode*) child;
if(pChild)
{
pChild->m_pParent = NULL;
}
}
}
// children
CC_SAFE_RELEASE(m_pChildren);
}
他幫咱們完成了這一步.咱們的玩家不是在Layer上的麼?Layer又在Scene上,當Scene被替換掉時,會自動執行他的析構函數,再執行父類的析構函數,也就是上面這段,這其中的m_pChildren中就保存着指向Layer的指針,他將Layer的父類賦值爲空,而後release掉m_pChildren.而這個m_pChildre是指向CCArray的指針, CCArray是CCObject的子類,初始化時引用計數被加了1,而後autorelease加入自動釋放池. m_pChildren被初始化爲NULL,他是這樣建立的
void CCNode::addChild(CCNode *child,int zOrder, int tag)
{
if( ! m_pChildren )
{
this->childrenAlloc();
}
}
void CCNode::childrenAlloc(void)
{
m_pChildren = CCArray::createWithCapacity(4);
m_pChildren->retain();
}
一來就被retain一次,通過一次pop引用計數爲1,因此不會被釋放.而最後的CC_SAFE_RELEASE(m_pChildren),將他的引用計數變爲0.執行delete this操做,進而執行析構函數.
CCArray::~CCArray()
{
ccArrayFree(data);
}
析構函數裏執行了一次釋放操做.
void ccArrayFree(ccArray*& arr)
{
if( arr == NULL )
{
return;
}
ccArrayRemoveAllObjects(arr);
free(arr->arr);
free(arr);
arr = NULL;
}
這裏又執行了一個remove操做,這個蒙了我好久的函數ccArrayRemoveAllObjects(arr),他用m_pChildren數組裏的成員執行一次release操做,也就是layer->release(),由於咱們的layer並無手動retain過.因此他的引用計數減1變爲0,而後執行delete this.回收內存.接着,保存layer的這個數組被free掉,而後m_pChildren被free掉,接着賦值爲NULL,完全刪除指針.
這樣一來,layer就完全沒有了.咱們以此類推,存在layer上的東西,也就是儲存在layer裏m_pChildren中的什麼CCSprite ,CCMenu,CCLabelTTF等等,都會在界面切換時被完全刪除掉.因此,內存管理不只僅只是autorelease作的事情,節點CCNode其實承擔了至關大一部份內存管理工做.相比起來,釋放池作的工做,僅僅是擔憂咱們使用局部指針變量時,忘記release的一種防範策略.
不過這也提醒了咱們,若是咱們new了一個局部的指針,而且手動retain了一下,那就必須在必要的地方手動release他一次,而且兩個操做的次數必須同樣.爲何呢?回顧一下上面分析的就知道了,最後僅僅只是release了一下而已,也就是說在不手動retain的狀況下,咱們的內存管理,最多能回收掉引用計數爲2的指針,若是你手動retain了,那最後的那個release不足以把引用計數減到0,那麼就內存泄露了………..
不過若是你執行的是退出遊戲,那就無所謂了,如今的操做系統,都能在程序退出時,將他所佔用的內存所有回收掉,就算是你new了一堆東西出來還不delete.
Cocos2d-x的內存管理到這裏就分析完了,雖然沒有我想一想的那樣智能,可是也讓我學到不少內存管理的思想.咱們下一篇內核分析再見~
後記: 寫完這篇文章後,我在作開發時又想了一下,其實這個自動釋放池不能算是坑,他的目的是把咱們new出來的指針的引用計數釋放到1,從而讓Scene切 換,Layer切換,退出程序時,真正的內存管理,也就是CCNode和CCObject的內存管理能順利吧全部的指針都釋放掉. 因此總結一下,此內存管理的思路是,用autorelease將指針的釋放計數控制在必定範圍內(最大值是2,autorelease以後必須最大是1),以致於當界面切換,各個類執行remove操做,程序退出等狀況下能將佔用的內存所有釋放掉.