不客气的说,大多数人诟病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