不客氣的說,大多數人詬病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被改變之外一切如常,引用計數也沒有變化。如果你不知道為什么沒有變化,請從頭再看一遍吧。
參考文獻
歡迎轉載,但請保留原文出處: http://www.cnblogs.com/xieheng/p/3617008.html