Cocos2d-x學習筆記(三) 內存管理


不客氣的說,大多數人詬病C++無非兩點

1)神出鬼沒的構造函數和析構函數

2)經常被遺忘的內存泄露

第一點是C++與生俱來的特(mao)點(bing),而第二點卻是由於編碼中各種各樣的人為原因造成的。是的,不客氣的說就是人為原因,但往往不是故意要讓內存泄露,只是不經意間就泄露了。所以編寫C++程序一半是在絞盡腦汁實現預期的功能,另一半是在小心翼翼的避免內存泄露。所幸的我們有不少的好辦法幫助我們避免內存泄露:

1)垃圾回收

2)智能指針

說到垃圾回收就能讓人立即想到Java或者C#,它們都是將垃圾回收機制作為語言特性。簡單來說垃圾回收就是運行一個后台線程監控每一個堆上的對象,在滿足一定的條件時銷毀對象回收內存(具體的內容可以google),所以銷毀對象的時機是不能預測的而且也可能不是及時的(如果你使用過C#的話應該會有體會吧)。對C++而言,實際使用中更常見的是使用智能指針來解決(boost中的share_ptr就一個最著名的例子),可喜的是cocos2d-x也實現了自己的智能指針機制。

 


以CCObject之名


 

在cocos2d-x的官方手冊上可以看到一張巨大的圖,展示了cocos2d-x中CCObject的繼承關系。因為只需要凸顯CCObject的地位,我進行了簡化

在cocos2d-x中CCObject是所有類的父類,在CCObject中使用引用計數實現了智能指針。

 1 class CC_DLL CCObject : public CCCopying
 2 {
 3 public:
 4     // object id, CCScriptSupport need public m_uID
 5     unsigned int        m_uID;
 6     // Lua reference id
 7     int                 m_nLuaID;
 8 protected:
 9     // count of references
10     unsigned int        m_uReference;
11     // is the object autoreleased
12     bool                m_bManaged;        
13 public:
14     CCObject(void);
15     virtual ~CCObject(void);
16     
17     void release(void);
18     void retain(void);
19     CCObject* autorelease(void);
20     CCObject* copy(void);
21     bool isSingleReference(void);
22     unsigned int retainCount(void);
23     virtual bool isEqual(const CCObject* pObject);
24 
25     virtual void update(float dt) { CC_UNUSED_PARAM(dt); }
26     
27     friend class CCAutoreleasePool;
28 };

首先需要說明,m_uID和m_nLuaID是在腳本引擎中使用,這里我們不關心它們。m_nReference保存了對象的引用計數,m_nManaged標識對象是否被托管。具體的細節我們后面會逐一分析。

 

 1.1 構造函數 

 1 CCObject::CCObject(void)
 2 {
 3     static unsigned int uObjectCount = 0;
 4 
 5     m_uID = ++uObjectCount;
 6     m_nLuaID = 0;
 7 
 8     // when the object is created, the reference count of it is 1
 9     m_uReference = 1;
10     m_bManaged = false;
11 }

 如前所述,我們並不關心m_uID和m_nLuaID。當一個對象被構造時,引用計數為1,而該對象並未被托管。

 

1.2 變化的引用計數

當對象被另一個變量每引用一次,其引用計數會增加1,以保證對象不會被錯誤銷毀。對應的函數是CCObject::retain

1 void CCObject::retain(void)
2 {
3     CCAssert(m_uReference > 0, "reference count should greater than 0");
4 
5     ++m_uReference;
6 }

當一個變量不再引用對象時,該對象的引用計數應當沒減1,以保證對象能及時被銷毀避免內存泄露。對應的函數是CCObject::release

 1 void CCObject::release(void)
 2 {
 3     CCAssert(m_uReference > 0, "reference count should greater than 0");
 4     --m_uReference;
 5 
 6     if (m_uReference == 0)
 7     {
 8         delete this;
 9     }
10 }

可以看到,當引用計數被減到0時就會銷毀該對象(對象自己把自己銷毀)。通過CCObject::retianCount函數可以獲取引用計數的值

1 unsigned int CCObject::retainCount(void)
2 {
3     return m_uReference;
4 }

需要強調的是:retain函數和release函數必須是成對使用的,也就是說調用多少次retain函數就必須調用多少次release函數,否則對象是不會被銷毀的,也就會造成內存泄露。

 


聰明的內存池


  

千古帝王往往都對應有着一位甚至幾位千古良相,優秀的游戲引擎都有着優秀的內存管理機制,而不是抱着智能指針睡大覺。因為只有這樣才能將開發者從可惡的內存問題里解放出來,專心對付設計游戲過程中更繁瑣的問題。

 

2.1 CCAutoreleasePool

在漫長的開發過程中我們會創建若干個對象,我們需要維護每個對象retain函數和release函數的成對關系有時候會變成不小的負擔。在cocos2d-x使用內存池來減小這種負擔

 1 class CC_DLL CCAutoreleasePool : public CCObject
 2 {
 3     CCArray*    m_pManagedObjectArray;    
 4 public:
 5     CCAutoreleasePool(void);
 6     ~CCAutoreleasePool(void);
 7 
 8     void addObject(CCObject *pObject);
 9     void removeObject(CCObject *pObject);
10 
11     void clear();
12 };

通過調用CCAutoreleasePool::addObject函數向內存池中添加托管的對象

1 void CCAutoreleasePool::addObject(CCObject* pObject)
2 {
3     m_pManagedObjectArray->addObject(pObject);
4 
5     CCAssert(pObject->m_uReference > 1, "reference count should be greater than 1");
6 
7     pObject->release(); // no ref count, in this case autorelease pool added.
8 }

被托管的對象被放進了m_pManagedObjectArray對象中,該對象是一個可變數組(CCArray)類型。在添加到數組中后,會馬上調用release函數將被托管對象的引用計數減1。這里就會引出一個問題:試想,如果一個對象被創建時其引用計數為1(見CCObject構造函數),然后立即被添加到內存池中,此時因為被調用了release函數,導致引用計數被減到0時對象被delete。這樣是會引發程序異常的。是不是代碼寫錯了呢?要回答這個疑問需要深入到CCArray::addObject函數內部可以看到對被托管對象的處理

1 /** Appends an object. Behavior undefined if array doesn't have enough capacity. */
2 void ccArrayAppendObject(ccArray *arr, CCObject* object)
3 {
4     CCAssert(object != NULL, "Invalid parameter!");
5     object->retain();
6     arr->arr[arr->num] = object;
7     arr->num++;
8 }

其他的代碼我們不用在意,只需要注意object->retain()這句代碼。也就是說在被托管對象被添加到m_pManageObjectArray中是其引用計數會增加1。所以在CCAutoreleasePool::addObject函數中把被托管對象添加到數組后立刻調用release函數其實並不會造成對象被析構,也不會引發程序異常。實際上,經過CCAutoreleasePool::addObject之后,被托管對象的引用計數並不會改變。

《道德經》有雲:故有無相生,難易相成,長短相形,高下相傾,音聲相和,前後相隨。同樣的道理,有“添加”就一定有“刪除”

1 void CCAutoreleasePool::removeObject(CCObject* pObject)
2 {
3     m_pManagedObjectArray->removeObject(pObject, false);
4 }

它將被托管對象從數組中刪除,CCArray::removeObject函數的最后一個參數決定了刪除對象的同時是否將對象析構:如果是true,刪除后析構對象;如果是false,只完成刪除並不析構對象。我們深入到CCArray::removeObject函數內部就清楚了

 1 /** Removes object at specified index and pushes back all subsequent objects.
 2  Behavior undefined if index outside [0, num-1]. */
 3 void ccArrayRemoveObjectAtIndex(ccArray *arr, unsigned int index, bool bReleaseObj/* = true*/)
 4 {
 5     CCAssert(arr && arr->num > 0 && index < arr->num, "Invalid index. Out of bounds");
 6     if (bReleaseObj)
 7     {
 8         CC_SAFE_RELEASE(arr->arr[index]);
 9     }
10     
11     arr->num--;
12     
13     unsigned int remaining = arr->num - index;
14     if(remaining>0)
15     {
16         memmove((void *)&arr->arr[index], (void *)&arr->arr[index+1], remaining * sizeof(CCObject*));
17     }
18 }

關鍵的代碼是6~9行,當然我們還需要搞清楚CC_SAFE_RELEASE的意思

1 #define CC_SAFE_RELEASE(p)            do { if(p) { (p)->release(); } } while(0)

將宏展開后代碼就變成了下面的樣子(去掉了我們不關系的內容)

 1 void ccArrayRemoveObjectAtIndex(ccArray *arr, unsigned int index, bool bReleaseObj/* = true*/)
 2 {
 3     // 其他代碼
 4 
 5     if (bReleaseObj)
 6     {
 7         do
 8         {
 9             if ((arr->arr[index]))
10             {
11                 arr->arr[index]->release();
12             }
13         } while(0);
14     }
15 
16     // 其他代碼
17 }

所以對於CCAutoreleasePool::removeObject函數只完成從數組中刪除對象,但並不銷毀對象。但是,我們知道“添加”托管對象時並不改變對象的引用計數,而對象的引用計數初始值為1。如果在“刪除”對象時不將對象引用計數減1,那么又是怎么保證對象被正確析構呢?答案在CCAutoreleasePool的析構函數中

1 CCAutoreleasePool::~CCAutoreleasePool(void)
2 {
3     CC_SAFE_DELETE(m_pManagedObjectArray);
4 }

CC_SAFE_DELETE宏定義如下

1 #define CC_SAFE_DELETE(p)            do { if(p) { delete (p); (p) = 0; } } while(0)

把宏展開后

 1 CCAutoreleasePool::~CCAutoreleasePool(void)
 2 {
 3     do
 4     {
 5         if (m_pManagedObjectArray)
 6         {
 7             delete m_pManagedObjectArray;
 8             m_pManagedObjectArray = 0;
 9         }
10     }
11      while (0);
12 }

本質上就是觸發CCArray析構函數被調用。深入CCArray的析構函數內部

1 /** Removes all objects from arr */
2 void ccArrayRemoveAllObjects(ccArray *arr)
3 {
4     while( arr->num > 0 )
5     {
6         (arr->arr[--arr->num])->release();
7     }
8 }

在這里,添加到數組內的所有對象都被調用了一次release函數。也就是說,所有對象在這里都被銷毀了。CCAutoreleasePool還提供了一個函數用於銷毀所有添加到CCAutoreleasePool對象中的被托管的對象

 1 void CCAutoreleasePool::clear()
 2 {
 3     if(m_pManagedObjectArray->count() > 0)
 4     {
 5         //CCAutoreleasePool* pReleasePool;
 6 #ifdef _DEBUG
 7         int nIndex = m_pManagedObjectArray->count() - 1;
 8 #endif
 9 
10         CCObject* pObj = NULL;
11         CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
12         {
13             if(!pObj)
14                 break;
15 
16             pObj->m_bManaged = false;
17             //(*it)->release();
18             //delete (*it);
19 #ifdef _DEBUG
20             nIndex--;
21 #endif
22         }
23 
24         m_pManagedObjectArray->removeAllObjects();
25     }
26 }

它遍歷每一個被托管對象,將它們的托管標識m_bManaged置為false。這里的循環方式很奇怪,既不是while也不是for,而是一個不屬於C++關鍵字的方式。我們來探究一下

1 #define CCARRAY_FOREACH_REVERSE(__array__, __object__)                                                          \
2     if ((__array__) && (__array__)->data->num > 0)                                                              \
3         for(CCObject** arr = (__array__)->data->arr + (__array__)->data->num-1, **end = (__array__)->data->arr; \
4             arr >= end && (((__object__) = *arr) != NULL/* || true*/);                                          \
5             arr--)

宏CCARRAY_FOREACH_REVERSE實際上就是一個for循環,該循環從數組的最后一個元素開始向前搜索。將宏展開后,CCAutoreleasePool::clear函數的原貌就清晰了

 1 void CCAutoreleasePool::clear()
 2 {
 3     if(m_pManagedObjectArray->count() > 0)
 4     {
 5         //CCAutoreleasePool* pReleasePool;
 6 #ifdef _DEBUG
 7         int nIndex = m_pManagedObjectArray->count() - 1;
 8 #endif
 9 
10         CCObject* pObj = NULL;
11         if (m_pManagerdObjectArray && m_pManagedObjectArray->data->num > 0)
12         {
13             for (CCObject **arr = m_pManagedObjectArray->data->arr + m_pManagedObjectArray->data->num-1, 
14                           **end = m_pManagedObjectArray->data->arr;
15                  arr >= end && ((pObj = *arr) != NULL/* || true*/);
16                  arr--)
17             {
18                 if(!pObj)
19                     break;
20 
21                 pObj->m_bManaged = false;
22                 //(*it)->release();
23                 //delete (*it);
24 #ifdef _DEBUG
25                 nIndex--;
26 #endif
27             }
28         }
29 
30         m_pManagedObjectArray->removeAllObjects();
31     }
32 }

在CCAutoreleasePool::clear函數的最后,析構所有被托管對象由m_pManagedObjectArray->removeAllObjects()完成。深入CCArray::removeAllObjects函數,其實現其實就是上面已經出現過的ccArrayRemoveAllObjects函數。

 

2.2 CCPoolManager

我們用CCAutoreleasePool來管理被托管的對象,使得在需要的時候能自動釋放所有被托管的對象。而在cocos2d-x中還定義了管理CCAutoreleasePool對象的類:CCPoolManager,來管理內存池對象。這里就很明白的高速了我們一件事:CCAutoreleasePool對象並非以單件形式存在的,這一點我們在搞清楚了CCPoolManager之后再來說。

CCPoolManager定義如下

 1 class CC_DLL CCPoolManager
 2 {
 3     CCArray*           m_pReleasePoolStack;    
 4     CCAutoreleasePool* m_pCurReleasePool;
 5 
 6     CCAutoreleasePool* getCurReleasePool();
 7 public:
 8     CCPoolManager();
 9     ~CCPoolManager();
10     void finalize();
11     void push();
12     void pop();
13 
14     void removeObject(CCObject* pObject);
15     void addObject(CCObject* pObject);
16 
17     static CCPoolManager* sharedPoolManager();
18     static void purgePoolManager();
19 
20     friend class CCAutoreleasePool;
21 };

雖然CCAutoreleasePool對象不是以單件形式存在,但CCPoolManager卻是以單件形式存在的,我們可以全局地訪問到內存池管理對象。

1 CCPoolManager* CCPoolManager::sharedPoolManager()
2 {
3     if (s_pPoolManager == NULL)
4     {
5         s_pPoolManager = new CCPoolManager();
6     }
7     return s_pPoolManager;
8 }

當然,我們也可以全局的銷毀內存池管理對象(這里真是用了一個優(e)雅(xin)的單詞來作函數名:purge可以作“通便”解釋)

1 void CCPoolManager::purgePoolManager()
2 {
3     CC_SAFE_DELETE(s_pPoolManager);
4 }

之前提到過,CCAutoreleasePool對象可以存在很多個,並被CCPoolManager對象所管理。從CCPoolManager的頭文件可以看出,內存池對象一定是存儲在m_pReleasePoolStack數組對象中,但真正有時怎么管理這個數組的呢?我們從變量名可以看到答案就是push和pop函數。

1 void CCPoolManager::push()
2 {
3     CCAutoreleasePool* pPool = new CCAutoreleasePool();       //ref = 1
4     m_pCurReleasePool = pPool;
5 
6     m_pReleasePoolStack->addObject(pPool);                   //ref = 2
7 
8     pPool->release();                                        //ref = 1
9 }

push函數中,構造了一個CCAutoreleasePool對象並壓入到m_pReleasePoolStack數組中。這里出現了熟悉的一幕:pPool被壓入到數組后,被調用了release函數。這和CCAutoreleasePool::addObject函數一樣,pPool的引用計數不會被改變保持為1。調用push函數的過程如下圖所示

看起來如同棧的操作一般。而實際上CCPoolManager就是實現了CCAutoreleasePool的棧,m_pCurReleasePool就是棧頂指針。而對於棧,有push操作就有對應的pop操作。

 1 void CCPoolManager::pop()
 2 {
 3     if (! m_pCurReleasePool)
 4     {
 5         return;
 6     }
 7 
 8     int nCount = m_pReleasePoolStack->count();
 9 
10     m_pCurReleasePool->clear();
11  
12     if(nCount > 1)
13     {
14         m_pReleasePoolStack->removeObjectAtIndex(nCount-1);
15 
16 //      if(nCount > 1)
17 //      {
18 //          m_pCurReleasePool = m_pReleasePoolStack->objectAtIndex(nCount - 2);
19 //          return;
20 //      }
21         m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount - 2);
22     }
23 
24     /*m_pCurReleasePool = NULL;*/
25 }

先將“棧頂”的CCAutoreleasePool對象清空,如果棧中內存池數量大於1則將“棧頂”對象從“棧”中刪除,並將“棧頂”指針指向下一個內存池對象。這里有一個疑問,當原來的內存池對象從內存池管理對象中刪除的時候,它是否被析構了呢?深入到CCArray::removeObjectAtIndex函數,其實現就是我們之前看到過的ccArrayRemoveObjectAtIndex函數,它的參數中有一個是否析構對象的標識,該參數默認值為true(即執行析構)。從pop函數的實現可以看到,被刪除的內存池對象在從內存池管理對象中刪除時被析構了。調用pop函數的過程如下圖所示

在CCPoolManager中可能存在多個CCAutoreleasePool對象,而同時有沒有提供獲取CCAutoreleasePool對象的接口。我們有時怎么樣使用CCAutoreleasePool對象的呢?我們首先來看看添加托管對象的實現

1 void CCPoolManager::addObject(CCObject* pObject)
2 {
3     getCurReleasePool()->addObject(pObject);
4 }

其中,getCurReleasePool函數的實現如下

 1 CCAutoreleasePool* CCPoolManager::getCurReleasePool()
 2 {
 3     if(!m_pCurReleasePool)
 4     {
 5         push();
 6     }
 7 
 8     CCAssert(m_pCurReleasePool, "current auto release pool should not be null");
 9 
10     return m_pCurReleasePool;
11 }

如果m_pCurRelasePool為0(即棧為空),則往內存池管理對象中push一個CCAutoreleasePool對象並將m_pCurReleasePool指向新添加的對象,然后將m_pCurReleasePool返回。也就是說我們通過CCPoolManager添加的托管對象都是添加到“棧頂”的CCAutoreleasePool對象中。

刪除托管對象的過程相對要簡單一些,它不需要對內存池管理對象本身進行操作

1 void CCPoolManager::removeObject(CCObject* pObject)
2 {
3     CCAssert(m_pCurReleasePool, "current auto release pool should not be null");
4 
5     m_pCurReleasePool->removeObject(pObject);
6 }

在CCPoolManager對象被析構的時候,對包含的對象所處的處理也很有意思

 1 CCPoolManager::~CCPoolManager()
 2 {
 3     finalize();
 4  
 5     // we only release the last autorelease pool here 
 6     m_pCurReleasePool = 0;
 7     m_pReleasePoolStack->removeObjectAtIndex(0);
 8  
 9     CC_SAFE_DELETE(m_pReleasePoolStack);
10 }

函數finalize也是CCPoolManager的成員函數,其實現如下

 1 void CCPoolManager::finalize()
 2 {
 3     if(m_pReleasePoolStack->count() > 0)
 4     {
 5         //CCAutoreleasePool* pReleasePool;
 6         CCObject* pObj = NULL;
 7         CCARRAY_FOREACH(m_pReleasePoolStack, pObj)
 8         {
 9             if(!pObj)
10                 break;
11             CCAutoreleasePool* pPool = (CCAutoreleasePool*)pObj;
12             pPool->clear();
13         }
14     }
15 }

宏CCARRAY_FROEACH如同我們之前看到過的CCARRAY_FROEACH_RECERSE代表着一個for循環,不同的是CCARRAY_FROEACH是從數組第一個元素開始向后搜索

1 #define CCARRAY_FOREACH(__array__, __object__)                                                                     \
2     if ((__array__) && (__array__)->data->num > 0)                                                                 \
3         for(CCObject** arr = (__array__)->data->arr, **end = (__array__)->data->arr + (__array__)->data->num-1;    \
4             arr <= end && (((__object__) = *arr) != NULL/* || true*/);                                             \
5             arr++)

CCPoolManager::finalise函數所做的就是遍歷m_pReleasePoolStack,將每一個CCAutoreleasePool對象清空。注意,這里並沒有刪除它們也沒有析構它們。刪除和析構過程實際上是由CCArray的析構函數完成的,也就是CCPoolManager析構函數中那一句CC_SAFE_DELETE所做的事情(這個宏以前已經多次遇得到,這里就不再細講了)。只是,比較有意思的是CCPoolManager析構函數中間那兩句

1 // we only release the last autorelease pool here 
2 m_pCurReleasePool = 0;
3 m_pReleasePoolStack->removeObjectAtIndex(0);

將“棧頂”指針賦0比較好理解,但為什么還要專門把索引值為0的CCAutoreleasePool對象刪除並銷毀呢?這件事完全可以由CC_SAFE_DELETE宏完成。呵呵,我會告訴你這是一個美學問題嗎?從前面的實現我們可以看出兩個事實:

1)索引值為0的CCAutoreleasePool對象是由CCPoolManager對象自行構造並壓入“棧”中的

2)索引值為0的CCAutoreleasePool對象不受外界的控制,也就是說,無論你調用多少次pop函數都不會刪除它

為了與“生成並極力保護‘棧底’寵兒”的行為構成對稱關系,在析構函數中就呈現出了“親手將‘棧底’寵兒銷毀”的行為。雖說是具有美學觀點的行為,但實際上這樣編碼是的代碼更容易理解和維護。

 


托兒所的大門


 

每個小孩子第一次被送到托兒所/幼兒園的時候都會哭的令人心碎,不曉得那些被托管到CCAutoreleasePool里的對象是不是也哭的稀里嘩啦了。

 

3.1 從HelloWorld說起

還記得HelloWorld類中的create函數嗎?就是那個用CREATE_FUNC宏定義的函數。好吧,不管記沒記起來,我們都再次把它列出來(這里是把宏展開整理過后的代碼)

 1 static __TYPE__* create() 
 2 { 
 3     __TYPE__ *pRet = new __TYPE__(); 
 4     if (pRet && pRet->init()) 
 5     { 
 6         pRet->autorelease(); 
 7         return pRet; 
 8     } 
 9     else 
10     { 
11         delete pRet; 
12         pRet = NULL; 
13         return NULL; 
14     } 
15 }

請注意第6行那句代碼,它調用了對象的autorelease函數。不要忘記這段實際是一個宏定義,如過我們指定的__TYPE__沒有autorelease接口就會導致編譯錯誤。更重要的是在cocos2d-x中所有的類都是從CCObject派生的,只要在CCObject中提供這個接口就可以保證編譯能順利進行。當然,cocos2d-x也是這樣做的。但autorelease函數的作用是什么呢?哈哈,我會告訴你它是托兒所的大門嗎?

 

3.2 autorelease

這是一個耐人尋味的大門,但又簡單得出奇

1 CCObject* CCObject::autorelease(void)
2 {
3     CCPoolManager::sharedPoolManager()->addObject(this);
4 
5     m_bManaged = true;
6     return this;
7 }

詳細一點說,這個函數的執行過程是這樣的:

1)通過CCPoolManager::sharedPoolManager函數獲取內存池管理對象

2)通過CCPoolManager::addObject函數將被托管對象壓入到“棧頂”的內存池中

3)將托管標識m_bManaged置為true

4)將對象返回

對被托管對象而言,整個過程中除了m_bManaged被改變之外一切如常,引用計數也沒有變化。如果你不知道為什么沒有變化,請從頭再看一遍吧。

 


參考文獻


Cocos2d-x高級開發教程:制作自己的《捕魚達人》

 

 


歡迎轉載,但請保留原文出處: http://www.cnblogs.com/xieheng/p/3617008.html 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM