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

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


引用计数无需多言,以下简称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。


有更好的方法求告知。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页