Cocos2d-x内存管理-绕不过去的坎

发表于2015-11-30
评论0 478浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594

Cocos2d-x引擎的核心是用C 编写的,那对于所有使用该引擎的游戏开发人员来说,内存管理是一道绕不过去的坎。

关于Cocos2d-x内存管理,网上已经有了许多参考资料,有些资料写的颇为详实,因为在内存管理这块我不想多费笔墨,只是更多的将思路描述清 楚。

一、对象内存引用计数

Cocos2d-x内存管理的基本原理就是对象内存引用计数,Cocos2d-x将内存引用计数的实现放在了顶层父类CCObject中,这里将涉及引用计数的CCObject的成员和方法摘录出来:

  1. class CC_DLL CCObject : public CCCopying  
  2. {  
  3. public:  
  4.    … …  
  5. protected:  
  6.     // count of references  
  7.     unsigned int        m_uReference;  
  8.     // count of autorelease  
  9.     unsigned int        m_uAutoReleaseCount;  
  10. public:  
  11.     void release(void);  
  12.     void retain(void);  
  13.     CCObject* autorelease(void);  
  14.     … ….  
  15. }  
  16.   
  17. CCObject::CCObject(void)  
  18. : m_nLuaID(0)  
  19. , m_uReference(1) // when the object is created, the reference count of it is 1  
  20. , m_uAutoReleaseCount(0)  
  21. {  
  22.   … …  
  23. }  
  24.   
  25. void CCObject::release(void)  
  26. {  
  27.     CCAssert(m_uReference > 0, "reference count should greater than 0");  
  28.     –m_uReference;  
  29.   
  30.     if (m_uReference == 0)  
  31.     {  
  32.         delete this;  
  33.     }  
  34. }  
  35.   
  36. void CCObject::retain(void)  
  37. {  
  38.     CCAssert(m_uReference > 0, "reference count should greater than 0");  
  39.   
  40.      m_uReference;  
  41. }  
  42.   
  43. CCObject* CCObject::autorelease(void)  
  44. {  
  45.     CCPoolManager::sharedPoolManager()->addObject(this);  
  46.     return this;  
  47. }  



先不考虑autorelease与m_uAutoReleaseCount(后续细说)。计数的核心字段是m_uReference,可以看到:

* 当一个Object初始化(被new出来时),m_uReference = 1;
* 当调用该Object的retain方法时,m_uReference ;
* 当调用该Object的release方法时,m_uReference–,若m_uReference减后为0,则delete该Object。

二、手工对象内存管理

在上述对象内存引用计数的原理下,我们得出以下Cocos2d-x下手工对象内存管理的基本模式:

  1. CCObject *obj = new CCObject();  
  2. obj->init();  
  3. …. …  
  4. obj->release();  
  5.   
  6. 在Cocos2d-x中CCDirector就是一个手工内存管理的典型:  
  7.   
  8. CCDirector* CCDirector::sharedDirector(void)  
  9. {  
  10.     if (!s_SharedDirector)  
  11.     {  
  12.         s_SharedDirector = new CCDisplayLinkDirector();  
  13.         s_SharedDirector->init();  
  14.     }  
  15.   
  16.     return s_SharedDirector;  
  17. }  
  18.   
  19. void CCDirector::purgeDirector()  
  20. {  
  21.     … …  
  22.     // delete CCDirector  
  23.     release();  
  24. }  


三、自动对象内存管理

所谓的“自动对象内存管理”,指的就是哪些不再需要的object将由Cocos2d-x引擎替你释放掉,而无需你手工再调用Release方法。

自动对象内存管理显然也要遵循内存引用计数规则,只有当object的计数变为0时,才会释放掉对象的内存。

自动对象内存管理的典型模式如下:

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


一般我们通过一个单例模式创建对象,与手工模式不同的地方在于init后多了一个autorelease调用。这里再把autorelease调用的实现摘录一遍:

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


追溯addObject方法:

  1. // cocoa/CCAutoreleasePool.cpp  
  2.   
  3. void CCPoolManager::addObject(CCObject* pObject)  
  4. {  
  5.     getCurReleasePool()->addObject(pObject);  
  6. }  
  7.   
  8. void CCAutoreleasePool::addObject(CCObject* pObject)  
  9. {  
  10.     m_pManagedObjectArray->addObject(pObject);  
  11.   
  12.     CCAssert(pObject->m_uReference > 1, "reference count should be greater than 1");  
  13.      (pObject->m_uAutoReleaseCount);  
  14.     pObject->release(); // no ref count, in this case autorelease pool added.  
  15. }  
  16.   
  17. // cocoa/CCArray.cpp  
  18. void CCArray::addObject(CCObject* object)                                                                                                     
  19. {                                                                                                                                            
  20.     ccArrayAppendObjectWithResize(data, object);                               
  21. }    
  22.   
  23. // support/data_support/ccCArray.cpp  
  24. void ccArrayAppendObjectWithResize(ccArray *arr, CCObject* object)                                                                            
  25. {                                                                                                                     
  26.     ccArrayEnsureExtraCapacity(arr, 1);                                                                
  27.     ccArrayAppendObject(arr, object);                                           
  28. }  
  29.   
  30. void ccArrayAppendObject(ccArray *arr, CCObject* object)  
  31. {  
  32.     CCAssert(object != NULL, "Invalid parameter!");  
  33.     object->retain();  
  34.     arr->arr[arr->num] = object;  
  35.     arr->num ;  
  36. }  


调用层次挺深,涉及的类也众多,这里归纳总结一下。

Cocos2d-x的自动对象内存管理基于对象引用计数以及CCAutoreleasePool(自动释放池)。引用计数前面已经说过了,这里单说自动释放池。Cocos2d-x关于自动对象内存管理的基本类层次结构如下:

  1. CCPoolManager类 (自动释放池管理器)  
  2.     – CCArray*    m_pReleasePoolStack; (自动释放池栈,存放CCAutoreleasePool类实例)  
  3.          
  4. CCAutoreleasePool类  
  5.     – CCArray*    m_pManagedObjectArray; (受管对象数组)  

CCObject关于内存计数以及自动管理有两个字段:m_uReference和m_uAutoReleaseCount。前面在手工管理模式下,我只提及了m_uReference,是m_uAutoReleaseCount该亮相的时候了。我们沿着自动释放对象的创建步骤来看看不同阶段,这两个重要字段的值都是啥,代表的是啥含义:

  1. CCYourClass*pRet = new CCYourClass();    m_uReference = 1; m_uAutoReleaseCount = 0;  
  2. pRet->init();                           m_uReference = 1; m_uAutoReleaseCount = 0;  
  3. pRet->autorelease();                      
  4. m_pManagedObjectArray->addObject(pObject); m_uReference = 2; m_uAutoReleaseCount = 0;  
  5. (pObject->m_uAutoReleaseCount);          m_uReference = 2; m_uAutoReleaseCount = 1;  
  6.  pObject->release();                        m_uReference = 1; m_uAutoReleaseCount = 1;   

在调用autorelease之前,两个值与手工模式并无差别,在autorelease后,m_uReference值没有变,但m_uAutoReleaseCount被加1。

m_uAutoReleaseCount这个字段的名字很容易让人误解,以为是个计数器,但实际上绝大多数时刻它是一个标识的角色,以前版本代码中有一个布尔字段m_bManaged,似乎后来被m_uAutoReleaseCount替换掉了,因此m_uAutoReleaseCount兼有m_bManaged的含义, 也就是说该object是否在自动释放池的控制之下,如果在自动释放池的控制下,自动释放池会定期调用该object的release方法,直到该 object内存计数降为0,被真正释放。否则该object不能被自动释放池自动释放内寸,需手工release。这个理解非常重要,再后面我们能用到 这个理解。

四、自动释放时机

通过autorelease我们已经将object放入autoreleasePool中,那究竟何时对象会被释放呢?答案是每帧执行一次自动内存对象释放操作。

在“Hello,Cocos2d-x”一文中,我们讲过整个Cocos2d-x引擎的驱动机制在于GLThread的guardedRun函数,后者会 “死循环”式(实际帧绘制频率受到屏幕vertsym信号的影响)的调用Render的onDrawFrame方法实现,而最终程序会进入 CCDirector::mainLoop方法中,也就是说mainLoop的执行频率是每帧一次。我们再来看看mainLoop的实现:

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


这次我们要关注的不是drawScene,而是 CCPoolManager::sharedPoolManager()->pop(),显然在游戏未退出 (m_bPurgeDirecotorInNextLoop决定)的条件下,CCPoolManager的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.         m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount – 2);  
  16.     }  
  17. }  


真正释放对象的方法是m_pCurReleasePool->clear()

  1. void CCAutoreleasePool::clear()  
  2. {  
  3.     if(m_pManagedObjectArray->count() > 0)  
  4.     {  
  5.         CCObject* pObj = NULL;  
  6.         CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)  
  7.         {  
  8.             if(!pObj)  
  9.                 break;  
  10.   
  11.             –(pObj->m_uAutoReleaseCount);  
  12.         }  
  13.   
  14.         m_pManagedObjectArray->removeAllObjects();  
  15.     }  
  16. }  
  17.   
  18. void CCArray::removeAllObjects()       
  19. {     
  20.     ccArrayRemoveAllObjects(data);                      
  21. }  
  22.   
  23. void ccArrayRemoveAllObjects(ccArray *arr)                      
  24. {                         
  25.     while( arr->num > 0 )                        
  26.     {                      
  27.         (arr->arr[--arr->num])->release();                 
  28.     }                      
  29. }   

不出预料,当前自动释放池遍历每个“受控制”Object,–m_uAutoReleaseCount,并调用该object的release方法。

我们接着按释放流程来看看m_uAutoReleaseCount和m_uReference值的变化:

CCPoolManager::sharedPoolManager()->pop();  m_uReference = 0; m_uAutoReleaseCount = 0;

五、自动释放池的初始化

自动释放池本身是何时出现的呢?回顾一下Cocos2d-x引擎的初始化过程(android版),引擎初始化实在Render的onSurfaceCreated方法中进行的,我们不难追踪到以下代码:

  1. //hellocpp/jni/hellocpp/main.cpp  
  2. Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit {  
  3.      
  4.     //这里CCDirector第一次被创建  
  5.     if (!CCDirector::sharedDirector()->getOpenGLView())  
  6.     {  
  7.         CCEGLView *view = CCEGLView::sharedOpenGLView();  
  8.         view->setFrameSize(w, h);  
  9.   
  10.         AppDelegate *pAppDelegate = new AppDelegate();  
  11.         CCApplication::sharedApplication()->run();  
  12.     }  
  13. }  
  14.      
  15. CCDirector* CCDirector::sharedDirector(void)  
  16. {  
  17.     if (!s_SharedDirector)  
  18.     {  
  19.         s_SharedDirector = new CCDisplayLinkDirector();  
  20.         s_SharedDirector->init();    
  21.     }  
  22.   
  23.     return s_SharedDirector;  
  24. }  
  25.   
  26. bool CCDirector::init(void)  
  27. {  
  28.     setDefaultValues();  
  29.   
  30.     … …  
  31.   
  32.     // create autorelease pool  
  33.     CCPoolManager::sharedPoolManager()->push();  
  34.   
  35.     return true;  
  36. }  


六、探寻Cocos2d-x内核对象的自动化内存释放

前面我们基本了解了Cocos2D-x的自动化内存释放原理。如果你之前翻看过一些Cocos2d-x的内核源码,你会发现很多内核对象都是通过单例模式create出来的,也就是说都使用了autorelease将自己放入自动化内存释放池中被管理。

比如我们在HelloCpp中看到过这样的代码:

  1. //HelloWorldScene.cpp  
  2. bool HelloWorld::init() {  
  3.      …. ….  
  4.     // add "HelloWorld" splash screen"  
  5.     CCSprite* pSprite = CCSprite::create("HelloWorld.png");  
  6.   
  7.     // position the sprite on the center of the screen  
  8.     pSprite->setPosition(ccp(visibleSize.width/2   origin.x, visibleSize.height/2   origin.y));  
  9.   
  10.     // add the sprite as a child to this layer  
  11.     this->addChild(pSprite, 0);  
  12.     … …  
  13. }  


CCSprite采用自动化内存管理模式create object(cocos2dx/sprite_nodes/CCSprite.cpp),之后将自己加入到HelloWorld这个CCLayer实例 中。按照上面的分析,create结束后,CCSprite object的m_uReference = 1; m_uAutoReleaseCount = 1。一旦如此,那么在下一帧时,该object就会被CCPoolManager释放掉。但我们在屏幕上依旧可以看到该Sprite的存在,这是怎么回事呢?

问题的关键就在this->addChild(pSprite, 0)这行代码中。addChild方法实现在CCLayer的父类CCNode中:

  1. //  cocos2dx/base_nodes/CCNode.cpp  
  2. void CCNode::addChild(CCNode *child, int zOrder, int tag)  
  3. {  
  4.     … …  
  5.     if( ! m_pChildren )  
  6.     {  
  7.         this->childrenAlloc();  
  8.     }  
  9.   
  10.     this->insertChild(child, zOrder);  
  11.   
  12.     … …  
  13. }  
  14.   
  15. void CCNode::insertChild(CCNode* child, int z)  
  16. {  
  17.     m_bReorderChildDirty = true;  
  18.     ccArrayAppendObjectWithResize(m_pChildren->data, child);  
  19.     child->_setZOrder(z);  
  20. }  
  21.   
  22. void ccArrayAppendObjectWithResize(ccArray *arr, CCObject* object)  
  23. {  
  24.     ccArrayEnsureExtraCapacity(arr, 1);  
  25.     ccArrayAppendObject(arr, object);  
  26. }  
  27.   
  28. void ccArrayAppendObject(ccArray *arr, CCObject* object)  
  29. {  
  30.     CCAssert(object != NULL, "Invalid parameter!");  
  31.     object->retain();  
  32.     arr->arr[arr->num] = object;  
  33.     arr->num ;  
  34. }  


又是一系列方法调用,最终我们来到了ccArrayAppendObject方法中,看到了陌生而又眼熟的retain方法调用。

在本文开始我们介绍CCObject时,我们知道retain是CCObject的一个方法,用于增加m_uReference计数。而实际上retain还隐含着“保留”这层意思。

在完成this->addChild(pSprite, 0)调用后,CSprite object的m_uReference = 2; m_uAutoReleaseCount = 1,这很关键。

我们在脑子里再过一下自动释放池释放object的过程:–m_uReference, –m_uAutoReleaseCount。一帧之后,两个值变成了m_uReference = 1; m_uAutoReleaseCount = 0。还记得前面说过的m_uAutoReleaseCount的另外一个非计数含义么,那就是表示该object是否“受控”,现在值为0,显然不再受自动释放池的控制了,后续即便再执行100次内存自动释放,也不会影响到该object的存活。

后续要想释放这个“精灵”,我们还是需要手工调用release,或再调用其autorelease方法。

原文链接

著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台