cocos2d-x 從onEnter、onExit、 引用計數 談內存泄露問題

原文出自:http://blog.sina.com.cn/s/blog_a502f1a30101hh6h.html


在看這個之前,你先要了解onEnter , onExit 和 構造函數,析構函數在調用順序上面的區別
總的來說,順序如下


構造函數
{
}
onEnter
{
}
onExit
{
}
析構函數
{
}


講這個之前需要講一些在實踐中遇到的問題。前段時間在項目中fix掉兩個和內存相關的bug,然後就對這兩個方法存在有一些新的理解。


1 我們用的cocos2d-x的版本是cocos2d-1.0.1-x-0.12.0,那時候還沒有CCListView.cpp這個列表控件,所以項目組就自己寫了這個控件。我們知道列表控件的需要放圖片或者文字等等信息,所以必須寫成虛類。如下


struct IListItem:
{
    //how we draw this item
    virtual cocos2d::CCNode *createNode(float width) = 0;
    virtual ~IListItem() {}
};


對應的Item需要繼承IListView才能來編寫這個接口,如下例子


class FightItem : public IListItem
{
public:


FightItem(const DailyProperty &dp);
~FightItem();
virtual cocos2d::CCNode *createNode(float width);
virtual void onExit();
void selected();
void unselected();


static FightItem *itemWithFight(const DailyProperty &dp);
const DailyProperty &getDailyProperty() {return m_dp;}


private:
cocos2d::CCNode *m_node;
};


看完這個接口,如果我們申明瞭FightItem *item(下面都是用item來指FightItem實例),那麼item的釋放我們只能使用delete了。本來用delete也沒什麼關係,隨着需求的變化,我們需要用到一些高級的方法,例如在FigthItem裏面。這時候我們就需要調用CCCallFunc::actionWithTarget(this,callfunc_selector(FigthItem::change) 類似於這樣的方法。這裏就要求FightItem繼承CCObject才能使用callfunc_selector。所以FightItem的聲明被改成了


class FightItem : public IListItem, cocos2d::CCObject
{
public:


FightItem(const DailyProperty &dp);
~FightItem();
virtual cocos2d::CCNode *createNode(float width);
virtual void onExit();
void selected();
void unselected();


static FightItem *itemWithFight(const DailyProperty &dp);
const DailyProperty &getDailyProperty() {return m_dp;}


private:
cocos2d::CCNode *m_node;
void change();
};


到這裏,內存管理就開始出現問題了,就是release和delete的混用的問題,FightItem現在是由引用計數管理着,而CCListView裏面就用delete把FightItem釋放了,然後引用計數仍然認爲FightItem還沒釋放,這裏就是double free,釋放兩次的錯誤了。


到這裏,需要總結一句,框架引入了引用計數做內存管理的時候,儘量不用混用delete,否則其他人不知道混用了,就會出這種問題。


很自然的,CCListView裏面也採用 FightItem * item; item->release(),這樣的釋放機制,問題就決絕了。接着,下一波問題出現了。
再次出現的是泄露問題了。


我們需要在FightItem 裏面播放一個動畫,這時我們有個地方就用瞭如下的方法


CCScheduler::sharedScheduler()->scheduleSelector(
          schedule_selector(FightItem::showTimeDescription), this, 0.5f, false);


好的,現在我們的析構函數這樣寫


FightItem::~FightItem()
{
CCScheduler::sharedScheduler()->unscheduleSelector(
schedule_selector(FightItem::showTimeDescription), this);
    if(m_node) m_node->release();
}


這樣看不出來有什麼問題是吧,不過在偶然的情況下,我發現這個析構函數根本不執行。真的很偶然,這樣的泄露確實是很難找出來的,所以先建立起這個意識確實很重要,等到項目代碼多了出這種問題,就要整個項目排查了。下面分析一下爲什麼不執行。


FightItem創建的時候count[引用計數]爲1, 執行
CCScheduler::sharedScheduler()->scheduleSelector(
          schedule_selector(FightItem::showTimeDescription), this, 0.5f, false);
的時候,引用計數爲2。說明CCScheduler佔用的了FightItem。按照原來的刪除過程,CCListView執行item->release(),FightItem引用計數變爲1,還被CCScheduler佔用。按照C++的機制,delete沒有執行,~FightItem也沒有進入所以
CCScheduler::sharedScheduler()->unscheduleSelector(
schedule_selector(FightItem::showTimeDescription), this);
函數雖然寫在裏面了,可是沒有執行的機會,這樣item永遠沒有釋放的機會。聽起來像不像數據庫裏面的死鎖啊。


終於可以開始寫主題了,這時候我們就需要虛函數onExit了,分寫修改如下

struct IListItem:public cocos2d::CCObject
{
    //how we draw this item
    virtual cocos2d::CCNode *createNode(float width) = 0;
    virtual ~IListItem() {}
virtual void onExit() {}
};


FightItem::~FightItem()
{
    if(m_node) m_node->release();
}


void FightItem::onExit()
{
CCScheduler::sharedScheduler()->unscheduleSelector(
schedule_selector(FightItem::showTimeDescription), this);
}


這時候CCListView執行item->release()改成這樣
item->onExit()
item->release();
onExit()把CCScheduler的引用清掉,接着CCListView執行release(),這時候引用計數爲0,系統就會執行delete了,這時候~FightItem()就會進入了。


這時候就可以得出最後結論了,onEnter,onExit是配合引用計數機制存在的,總的來說,所有造成本對象被其他對象引用的操作,都放在onEnter裏面,所有去掉本對象被其他對象引用的操作都放在onExit裏面,這樣就能保證本對象在釋放的時候能夠成功了。


所以,我們一般都是寫成如下的規範,就很少出現內存泄露問題了


void CCImageBtn::onEnter()
{
CCNode::onEnter();
    CCTouchDispatcher::sharedDispatcher()->addTargetedDelegate(this, m_touchPriority, true);
}


void CCImageBtn::onExit()
{
    CCTouchDispatcher::sharedDispatcher()->removeDelegate(this);
    CCNode::onExit();
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章