Cocos2Dx之内存管理-欧阳左至

Cocos2Dx的所有的类都直接或间接继承自CCObject,让类具有自动的内存管理能力。根本上讲,是借鉴自苹果的设计思维。从iPhone版的Cocos2Dx,到C++版的Cocos2Dx,以及到后面的JS,HTML5,保持API的一致性可以帮助开发者快速适应或者选择新的开发语言。沿袭的Objective-C风格个人觉得很不错,采用C++开发就是需要有自己的风格,或者叫规范,来指导开发者,避免迷失在C++的丛林中。

Cocos2Dx的内存管理有两个思路:一是引用计数,二是自动回收池。引用计数来记录对象是否还被引用,如果没有被引用就释放对象。自动回收池是将对象的引用释放进行了托管。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class  CC_DLL CCObject :  public  CCCopying
{
public :
     unsigned  int  m_uID;
protected :
     unsigned  int  m_uReference;
     unsigned  int  m_uAutoReleaseCount;
public :
     CCObject( void );
     virtual  ~CCObject( void );
     void  release( void );
     void  retain( void );
     CCObject* autorelease( void );
     bool  isSingleReference( void const ;
     unsigned  int  retainCount( void const ;
     friend  class  CCAutoreleasePool;
};

CCObject的release和retain实现引用计数功能;autorelease实现回收池功能。为了简单起见,我们抛开CCObject的其他部分,以及跟脚本相关的部分。C++的对象构造是从构造函数开始的,我们先看看构造函数。

?
1
2
3
4
5
6
7
CCObject::CCObject( void )
: m_uReference(1) 
, m_uAutoReleaseCount(0)
{
     static  unsigned  int  uObjectCount = 0;
     m_uID = ++uObjectCount;
}

当一个对象构造出来的时候,它的引用计数(m_uReference)初始化为1。初始化出来没有放到自动回收池里面,所以它的自动引用计数(m_uAutoReleaseCount)是0。有趣的是,CCObject还有一个公共成员m_uID,它是对象全局唯一的ID。你可能对全局ID在多线程环境中有所担忧,但是现在我们主要使用的还是单线程。

对象构造出来了,什么时候释放呢?

这里就需要解释一下Cocos2Dx借鉴自Object-C的对象初始化和释放方式。C++一般的做法是,先new/new[],然后delete/delete[]。Cocos2Dx的对象构造一般不直接调用new,而是通过类的静态函数create(),它内部再去new

一个自身。对象的初始化并不是直接依赖于构造函数,而是使用init函数。

我们看下CCSprite的create函数:

?
1
2
3
4
5
6
7
8
9
10
11
12
CCSprite* CCSprite::create()
{
     CCSprite *pSprite =  new  CCSprite();
     if  (pSprite && pSprite->init())
     {
         pSprite->autorelease();
         return  pSprite;
     }
     CC_SAFE_DELETE(pSprite);
     return  NULL;
}
#define CC_SAFE_DELETE(p) do { if(p) { delete (p); (p) = 0; } } while(0)

Cocos2Dx中new一个对象只是申请了内存空间和初始化了访问计数。对象本身的初始还需要调用对象提供的init来完成。在new出一个CCSprite并且调用init()做了对象初始化之后,调用了CCObject的autorelease(),将其放到自动回收池当中。

?
1
2
3
4
5
CCObject* CCObject::autorelease( void )
{
     CCPoolManager::sharedPoolManager()->addObject( this );
     return  this ;
}

CCPoolManager::sharedPoolManager()返回对象池Manager单例。单例模式的使用在Cocos2Dx也很多。本质上单例就是全局变量,提供一个封装好的入口而已。CCPoolManager维护了一个栈式的对象池。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void  CCPoolManager::addObject(CCObject* pObject)
{
     getCurReleasePool()->addObject(pObject);
}
CCAutoreleasePool* CCPoolManager::getCurReleasePool()
{
     if (!m_pCurReleasePool)
     {
         push();
     }
     return  m_pCurReleasePool;
}
void  CCPoolManager::push()
{
     CCAutoreleasePool* pPool =  new  CCAutoreleasePool();  //CCAutoreleasePool的CCObject的引用计数是1
     m_pCurReleasePool = pPool;
     m_pReleasePoolStack->addObject(pPool);  //内部调用pPool的retain(),将CCObject的引用计数是暂时提升到2
     pPool->release();  //CCAutoreleasePool的CCObject的引用计数是1。需要注意的是CCAutoreleasePool虽然是一个对象容器,但是并没有增加对象的引用计数,只是增加了自动引用计数
}

CCPoolManager的push()和pop()负责维护对象自动回收池栈。CCPoolManager内部使用的是CCArray来做数据存储。栈顶位于数组的后面(索引值大),栈底位于数组的前面。调用autorelease()将对象添加到当前栈顶的对象池当中时,如果当前对象回收池池m_pCurReleasePool为空,就调用push创建一个对象回收池,然后将其添加到CCPoolManager中。CCPoolManager返回当前自动回收池给CCObject的autorelease()添加对象。

注意push()的代码,由于使用的new,我们就需要自己维护好引用计数。所有容器的addObject方法,内部都会增加添加对象的引用计数。这是可以理解的,因为相对于现在容器也有了到对象的引用。push()后面又调用了release(),是因为pPool指针对其的引用来跳出代码块以后已经无效,从外部看,只有容器对其存在唯一的引用。这就是对象所有权的传递。所有权传递需要特别小心,不然会导致引用计数错误,进而使对象过早被释放,甚至不再被释放。管理内存是额外的负担,如果没有合适的理由,不要自己去new对象,然后维护引用计数。繁琐的细节应该被隐藏起来。

?
1
2
3
4
5
6
void  CCAutoreleasePool::addObject(CCObject* pObject)
{
     m_pManagedObjectArray->addObject(pObject);
     ++(pObject->m_uAutoReleaseCount);
     pObject->release(); 
}

CCAutoreleasePool内部也是通过CCArray来存储被托管进来的对象。CCArray的addObject(pObject)会将对象pObject的引用计数加1,所以后面会调用release()将引用计数减1。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void  CCArray::addObject(CCObject* object)
{
     ccArrayAppendObjectWithResize(data, object);
}
void  ccArrayAppendObjectWithResize(ccArray *arr, CCObject* object) 
     ccArrayEnsureExtraCapacity(arr, 1); 
     ccArrayAppendObject(arr, object); 
}
void  ccArrayAppendObject(ccArray *arr, CCObject* object) 
     object->retain(); 
     arr->arr[arr->num] = object; 
     arr->num++; 
}

现在是时候看看release()和retain()的实现了。

?
1
2
3
4
5
6
7
8
9
10
11
12
void  CCObject::release( void )
{
     --m_uReference;
     if  (m_uReference == 0)
     {
         delete  this ;
     }
}
void  CCObject::retain( void )
{
     ++m_uReference;
}

release()和retain()就是将引用计数加1和减1。
到现在我们看到release()会删除对象,autorelease()会将对象添加到自动回收池当中。但是什么对象回收池什么时候去真正释放托管的那些对象呢?我们是不是需要一个单独地线程来做对象回收池回收?Cocos2Dx在CCDisplayLinkDirector::mainLoop(void)当中调用CCPoolManager::sharedPoolManager()->pop()完成一次回收池释放的。

我们以WIN32为例,看看Cocos2Dx的消息循环是如何处理对象回收池的。

应用的消息循环可以分为两层:

  • 一是CCApplication::run()。主要完成操作系统相关的消息获取、分发等功能。这一层是平台相关的,每个平台有自己不同的实现。

  • 二是CCDirector::sharedDirector()->mainLoop()。这是Cocos2Dx的主循环,负责游戏画面的绘制、事件调度等。这一层是Cocos2Dx的核心运行机制。这一层跟前一层是嵌套关系,但run内的循环获取到操作系统消息一次,并不对应着mainLoop调用一次。mainLoop的调用时机由游戏的帧间隔决定,如果没有达到帧间隔时间,mainLoop是不会被调用的。

完整的启动流程我们在下一篇文章中讨论。现在直接看CCDirector::sharedDirector()->mainLoop()。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
void  CCDisplayLinkDirector::mainLoop( void )
{
     if  (m_bPurgeDirecotorInNextLoop)
     {
         m_bPurgeDirecotorInNextLoop =  false ;
         purgeDirector();
     }
     else  if  (! m_bInvalid)
      {
          drawScene();
          CCPoolManager::sharedPoolManager()->pop();        
      }
}

mainLoop前面判断是否调用了CCDirector的end(),即游戏结束。如果是,CCDirector做一些清理工作,否则就绘制场景,并且调用CCPoolManager::pop()来释放一次内存池。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
     }
}

pop() 首先检查当前是否有活动的对象回收池,如果没有就什么都不做。如果我们没有调用过autorelease(),没有去托管一些对象到对象回收池,就会存在这种情况。然后调用对象回收池CCAutoreleasePool的clear()函数。clear()遍历对象回收池,将每个对象的m_uAutoReleaseCount减1。然后将这些对象从对象回收池的内部存储容器CCArray中删除。

调用了pop(),我们只是岁栈顶的对象回收池进行了释放。栈里面的其他对象池仍然存在,没有任何变化。但需要调整一下当前对象回收池,m_pCurReleasePool 应该是现在位于栈顶的回收池了。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void  CCAutoreleasePool::clear()
{
     if (m_pManagedObjectArray->count() > 0)
     {
         CCObject* pObj = NULL;
         CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
         {
             if (!pObj)
                 break ;
             --(pObj->m_uAutoReleaseCount);
         }
         m_pManagedObjectArray->removeAllObjects();
     }
}

前面看到CCArray::addObject会调用retain增加引用计数,与此对应,CCArray的所有删除接口都会调用对象的release()来减少引用计数,并且在引用计数为0的时候释放对象。

?
1
2
3
4
5
6
7
8
9
10
11
void  CCArray::removeAllObjects()
{
     ccArrayRemoveAllObjects(data);
}
void  ccArrayRemoveAllObjects(ccArray *arr)
{
     while ( arr->num > 0 )
     {
         (arr->arr[--arr->num])->release();
     }
}

通过前面的分析,可以看到,retain()和release()分别用来获取对象和释放对象。autorelease()用来添加对象到自动回收池,对象回收池在每隔帧间隔时间统一释放一次回收池内的对象。单单使用这三个函数,开发者还是有内存维护的代价在里面。Cocos2Dx进一步利用一些编码指导规范来帮助大家简化内存维护的代价。

前面提到过,自己通过new来构造一个对象需要自己做一些额外的工作。推荐使用静态的create()来构造对象。create()构造对象都是先new出对象,然后调用对象的init()函数做初始化,然后添加到对象回收池。Cocos2Dx提供了CREATE_FUNC来帮助我们完成create()函数的。使用非常方便,只需要在类的public成员里面插入这样一条宏即可。

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

有意思的是,Cocos2Dx还提供了一些其他非常有用的宏:CC_SYNTHESIZE帮助创建Setter/Getter,CCARRAY_FOREACH帮助遍历数组,CC_SAFE_DELETE等帮助释放对象,CC_BREAK_IF做条件判断。还有很多宏,可以学习借鉴下。

最后我们用例子来结束内存管理这部分。

实例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static  CCLayer* backAction()
{
     sceneIdx--;
     int  total = MAX_LAYER;
     if ( sceneIdx < 0 )
         sceneIdx += total;
     CCLayer* pLayer = (createFunctions[sceneIdx])();           //构造一个CCLayer,没有使用CREATE_FUNC宏,引用计数为1,自动引用计数为0
     pLayer->autorelease();         //添加到对象回收池当中,引用计数为1,自动引用计数为1
     return  pLayer;
}
void  LayerTest::backCallback(CCObject* pSender)
{
     CCScene* s =  new  LayerTestScene();     //创建一个LayerTestScene对象,引用计数为1,自动引用计数为0
     s->addChild( backAction() );             //CCLayer的引用计数增加1,引用计数为2,自动引用计数为1
     CCDirector::sharedDirector()->replaceScene(s);     //LayerTestScene的引用计数增加1,引用计数为2,自动引用计数为0
     s->release();     //LayerTestScene的引用计数减少1,引用计数为1,自动引用计数为0
}

CCLayer的引用计数为2,自动引用计数为1。在过一个帧间隔之后,CCAutoreleasePool::pop()被调用,进而调用CCAutoreleasePool::clear(),最终调用CCArray::removeAllObjects(),对所有CCArray中的对象调用一次release()。CCLayer的引用计数为1,自动引用计数为0。LayerTestScene继承结构中有CCNode,它的析构函数会释放掉所有的孩子节点:

CC_SAFE_RELEASE(m_pChildren)

    ->CCArray.release()

        ->ccArrayFree(data)

            ->ccArrayRemoveAllObjects(arr)

                ->CCLayer.release()

CCLayer现在的引用计数为1,自动引用计数为0,调用release()就会释放CCLayer。可以看到CCLayer的释放,依赖于父节点LayerTestScene的释放。

LayerTestScene的引用计数为1,自动引用计数为0。当有新的Scene需要显示的时候,会做Scene的切换,旧的Scene会被CC_SAFE_RELEASE删除(调用release())。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值