cocos2d-x 內存管理

不過在這以前,咱們要先引入另外一個類.Cocos2d-x的核心類之一CCDirector.這個類能夠說是引擎最主要的類了,沒有他,引擎內的全部東西都沒法運轉起來.因爲這個類有點大,作的事情不少,我就不一一粘出來看了,咱們只關心跟內存管理有關的函數.
  由於一部片子只須要一個導演,因此CCDirector也就是一個單例類,是多重繼承自CCObjectTypeInfo,有什麼用,暫時無論.不過是採用的二段構建的單例.什麼是二段構建,說簡單點,就是不在構造函數中作初始化工做,而在另外一個函數裏作初始化工做.這樣作有什麼好處?能夠說沒有好處,也能夠說有好處.的看用的人是出於一種什麼樣的目的了.好比說咱們這裏,若是不用二段構建是不可能實現的.由於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,他只有01兩個值,你把它定成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的開發者也不會叫咱們儘可能不要用自動釋放.

好啦,重要的分析結束啦!

下面呢,我就把整個自動管理的過程串起來,給你們一個清晰的思路了.

  1. 咱們首先new一個類出來  CCSprite *pSprite = CCSprite::create(),這個靜態工廠方法裏,直接就執行了autorelease操做了.

  2. ,這裏面的過程就是這樣的:new操做將引用計數初始化爲1,釋放計數初始化爲0, autorelease經過幾回操做,將咱們的pSprite,也就是他的內存地址存放進了自動釋放池裏.而後retain將引用計數加1.這時引用計數爲2.這時,咱們就能夠理解,爲何後面有一個pObject->release()操做了.咱們只是想把他加入自動管理,但並不想retain.因而乎,引用計數仍是1.

  3. 這時,咱們執行addChild(pSprite),將咱們的精靈加入節點中,這個操做也會將引用計數加1.來給你們看看代碼.

  4. ……………………..

  5. ,我只挑重要的函數講,這裏執行了一個insertChild操做.

void CCNode::insertChild(CCNode* child, int z)

{

   m_bReorderChildDirty = true;

   ccArrayAppendObjectWithResize(m_pChildren->data, child);

   child->_setZOrder(z);

}

  1. ,咱們上一篇才提到的函數ccArrayAppendObjectWithResize又出現了,他的出現,就意味着retain的出現.我就不繼續粘代碼了,那這裏的m_pChildren就必定是一個CCArray的指針了.這時pSprite的引用計數爲2.

  2. 假設這時咱們的遊戲中有且僅有一個精靈,也就是咱們的pSprite,咱們暫時把他當作是玩家,遊戲中,玩家是咱們主要操做的對象,加入自動釋放後,就有被自動釋放的可能,可是這是咱們所不容許的.引擎天然也知道則一點.因此,這時候mainLoop執行一次,pop執行一次,咱們當前有自動釋放池,因此clear執行一次.clear,釋放計數被減1,歸零,而後因爲removeAllObjects的執行,咱們的玩家pSprite執行一次release,引用計數減由2變成1.而後數組被強行歸零.這時mainLoop再次執行的話,釋放池中的元素個數就爲0(沒有再添加其餘的東西).

  3. ,這不是坑爹嗎?釋放池就是用來釋放指針的,可是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的父類賦值爲空,而後releasem_pChildren.而這個m_pChildre是指向CCArray的指針, CCArrayCCObject的子類,初始化時引用計數被加了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_pChildrenfree,接着賦值爲NULL,完全刪除指針.

這樣一來,layer就完全沒有了.咱們以此類推,存在layer上的東西,也就是儲存在layerm_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操做,程序退出等狀況下能將佔用的內存所有釋放掉.

相關文章
相關標籤/搜索