所有的代码都已经屏蔽掉无关部分,仅展示对问题有实质影响的部分;
引用计数无需多言,以下简称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。
有更好的方法求告知。