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