cocos2dx 看上去很美的引用计数造成的内存泄露(一)——CCCallFunc对象

来源:互联网 发布:靠谱的水乳推荐 知乎 编辑:程序博客网 时间:2024/06/06 09:19

所有的代码都已经屏蔽掉无关部分,仅展示对问题有实质影响的部分;


引用计数无需多言,以下简称RC。

先说RC的2个基本原则:

1、不直接new和delete对象,而是通过RC实现,RC为0,对象销毁。在cocos2dx中通过retain,release,autoRelease实现。

2、要使用一个对象,先retain,用完release,因为如果不这么做,说不定正在用的东西不知道被谁释放了。尤其是有autoRelease的存在。

如果大家都严格遵照这样2个原则,那么内存就不会泄露了。


以前也是这么认为,但是后来我发现这只是看上去很美。


深受android “毒害” 的我最近在项目中遇到了这么一个问题,内存占用只加不减,多切换几个场景,就会因为内存问题被系统杀掉。断点怒跟,发现是当前场景并没有调用析构。也就是说应该销毁的场景没销毁,造成了内存泄露。

原因是我自定义了一个button控件,如下:

class LCButton : public CCControl{public:void init(const char* normalPath  , const char* highlightPath , const char* disablePath , CCCallFunc *active , CCCallFunc *disactice);static LCButton* create(const char* normalPath  , const char* highlightPath , const char* disablePath , CCCallFunc *active , CCCallFunc *disactice , int priority = -127);void setDownSelector(CCCallFunc *call);void setUpSelector(CCCallFunc *call);private:CCCallFunc *beginSelector;CCCallFunc *endSelector;};
其中对于点击事件的回调,我是用CCCallfunc及其派生子类实现,因为我觉得CCCallfunc这个类存在的意义,就是拿来做回调。那么根据上面提到的RC原则,我就应该这么写:

void LCButton::setDownSelector(CCCallFunc *call){if(beginSelector){beginSelector->release();}beginSelector = call ;CC_SAFE_RETAIN(beginSelector);}
在这个对象的存在的生命周期内,这个回调都应该起作用,释放内存的工作应该放到析构进行,那么我就应该在析构对beginSelector进行释放:

LCButton::~LCButton(void){CC_SAFE_RELEASE(beginSelector);}
然后在外部,使用这个组件的类,我也是这么处理的,create出来之后retain,析构release释放内存。

问题就在这里,看看CCCallfunc的create函数:

CCCallFunc * CCCallFunc::create(CCObject* pSelectorTarget, SEL_CallFunc selector) {    CCCallFunc *pRet = new CCCallFunc();    if (pRet && pRet->initWithTarget(pSelectorTarget)) { }}
bool CCCallFunc::initWithTarget(CCObject* pSelectorTarget) {    if (pSelectorTarget)     {        pSelectorTarget->retain();    }    if (m_pSelectorTarget)     {        m_pSelectorTarget->release();    }}
对于传入的this指针,做了一次retain。

那么这个callfunc对象,就对外部场景this保存了一次引用,当callfunc对象释放,释放this的引用。但是callfunc又被我这个button控件持有,button释放释放callfunc,但是button又被this持有,this释放释放button。

整理一下,就是  this->button->callfunc->this这么一个循环引用关系。相当于一个死锁,大家都释放不掉。


要解决这个问题,最简单的就是把对持有child的release提前,比如提前到onExit里面去做。但是这和设计理念有违背,因为释放内存这是应该是析构来做的事情。onExit只做清理。看cocos2dx源码里面的析构和onExit函数:

CCNode::~CCNode(void){    CC_SAFE_RELEASE(m_pActionManager);    CC_SAFE_RELEASE(m_pScheduler);    CC_SAFE_RELEASE(m_pCamera);    CC_SAFE_RELEASE(m_pGrid);    CC_SAFE_RELEASE(m_pShaderProgram);    CC_SAFE_RELEASE(m_pUserObject);    CC_SAFE_RELEASE(m_pChildren);    m_pComponentContainer->removeAll();    CC_SAFE_DELETE(m_pComponentContainer);}
void CCNode::onExit(){    this->pauseSchedulerAndActions();    m_bRunning = false;    arrayMakeObjectsPerformSelector(m_pChildren, onExit, CCNode*);    }
也是在onExit里面做清理,析构做内存释放。


那么还有一种,就是在CCCallfunc里面不做retain,看CCMenu里面的create:

CCMenuItem* CCMenuItem::create(CCObject *rec, SEL_MenuHandler selector){    CCMenuItem *pRet = new CCMenuItem();    pRet->initWithTarget(rec, selector);    pRet->autorelease();    return pRet;}bool CCMenuItem::initWithTarget(CCObject *rec, SEL_MenuHandler selector){    setAnchorPoint(ccp(0.5f, 0.5f));    m_pListener = rec;    m_pfnSelector = selector;    m_bEnabled = true;    m_bSelected = false;    return true;}
里面就没有对this进行retain处理。但是又觉得,有封装好了的回调类不用,又单独做一个,重复造轮子的行为。


而又看CCCallfunc,对this的一次retain在某些情况下又是必要的。比如做一个全局schedule调度的时候,this如果被释放了就什么都没有了。



最后还是选择了最容易实现的第一种方法,释放提前到onExit。


有更好的方法求告知。

0 0