cocos2d-x 从onEnter、onExit、 引用计数 谈内存泄露问题

本文通过解决Cocos2d-x项目中的内存泄漏问题,介绍了onEnter与onExit方法的正确使用方式,并探讨了引用计数机制下如何避免内存泄漏。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

///////////////////////////////////
//author : zhxfl
//date   : 2013.8.29
//email  : 291221622@qq.com
///////////////////////////////////
 
在看这个之前,你先要了解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() {}
};
View Code
 
对应的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;
};
View Code
 
看完这个接口,如果我们申明了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();
};
View Code
 
到这里,内存管理就开始出现问题了,就是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);
View Code
 
好的,现在我们的析构函数这样写
 
FightItem::~FightItem()
{
    CCScheduler::sharedScheduler()->unscheduleSelector(
        schedule_selector(FightItem::showTimeDescription), this);
    if(m_node) m_node->release();
}
View Code
 
这样看不出来有什么问题是吧,不过在偶然的情况下,我发现这个析构函数根本不执行。真的很偶然,这样的泄露确实是很难找出来的,所以先建立起这个意识确实很重要,等到项目代码多了出这种问题,就要整个项目排查了。下面分析一下为什么不执行。
 
FightItem创建的时候count[引用计数]为1, 执行
CCScheduler::sharedScheduler()->scheduleSelector(
          schedule_selector(FightItem::showTimeDescription), this, 0.5f, false);
的时候,引用计数为2,加入CCListView引用计数为3(注意当引用计数为1的时候,会被系统直接释放掉)。说明CCScheduler占用的了FightItem。按照原来的删除过程,CCListView执行item->release(),FightItem引用计数变为2,还被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);
}
View Code
这时候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();
}
View Code

 

转载于:https://www.cnblogs.com/zhxfl/p/3288510.html

<think>我们正在处理一个关于Cocos2d-x3.10在Lua层开发中,当iOS应用切换后台时事件处理出现紊乱,并且self对象存在问题的情况。根据用户描述,问题出现在应用切后台时,事件处理异常,且self对象可能失效。分析可能的原因:1.应用进入后台时,Cocos2d-x引擎会暂停渲染和逻辑更新,这可能导致一些事件被挂起或丢失。2.在Lua层,当应用进入后台,Lua状态可能被保存,但在恢复时,某些对象的引用(如self)可能失效,导致回调时出现问题。3.事件监听(例如后台事件)可能在切后台时没有被正确处理,导致状态不一致。参考引用:[^1]提到了Lua绑定的生成,但与此问题关联不大。[^2]提到了iOS视频控件的设置,其中有关闭用户交互的代码,这可能对事件处理有启发。[^3]提到了Cocos2d-lua运行环境,包括Lua引擎的初始化。[^4]提到了Lua绑定的生成问题,与当前问题无关。因此,我们主要关注应用生命周期事件的处理,特别是进入后台和返回前台的事件。在Cocos2d-x中,应用生命周期事件通过Director和EventCustom来通知。对于Lua层,我们可以监听以下事件:-"EVENT_COME_TO_FOREGROUND":应用回到前台-"EVENT_COME_TO_BACKGROUND":应用进入后台但是,用户反映在切后台时事件处理紊乱,self对象有问题,可能是由于在事件回调中使用了已经失效的对象(比如节点已经被移除,但回调仍然被调用)。解决方案建议:1.确保在进入后台时,正确地暂停游戏逻辑,并在回到前台时恢复。同时,移除一些在后台不需要的事件监听,并在回到前台时重新注册。2.检查self对象的问题:在Lua中,self可能是一个table,如果这个table在进入后台后被销毁(比如场景切换),那么回到前台时再使用就会出错。因此,在事件回调中,需要检查self的有效性。具体步骤:步骤1:在Lua中监听应用进入后台和回到前台的事件,并在这些事件中处理游戏状态。例如:```lualocalfunctiononEnterBackground()--暂停游戏逻辑、声音等cc.Director:getInstance():pause()--移除可能引起问题的监听器(例如触摸事件,因为后台时不应该响应)--同时,标记当前状态为后台,避免后续回调操作endlocalfunctiononEnterForeground()--恢复游戏cc.Director:getInstance():resume()--重新注册必要的监听器endlocaleventDispatcher=cc.Director:getInstance():getEventDispatcher()self.backgroundListener=cc.EventListenerCustom:create("EVENT_COME_TO_BACKGROUND",onEnterBackground)eventDispatcher:addEventListenerWithFixedPriority(self.backgroundListener,1)self.foregroundListener=cc.EventListenerCustom:create("EVENT_COME_TO_FOREGROUND",onEnterForeground)eventDispatcher:addEventListenerWithFixedPriority(self.foregroundListener,1)```注意:这里的self应该是当前场景或节点,确保在场景退出时移除这些监听器,否则当场景销毁后,回调函数中的self就无效了。步骤2:解决self对象失效问题。在回调函数中,使用弱引用或者确保在对象销毁时移除监听器。在Lua中,我们可以使用弱引用表来避免持有对象的强引用导致无法回收,但更常见的做法是在对象销毁时(如节点的onExit函数)移除监听器。例如,在节点中:```luafunctionMyScene:onEnter()--添加监听器self.backgroundListener=...--如上eventDispatcher:addEventListenerWithFixedPriority(self.backgroundListener,1)endfunctionMyScene:onExit()--移除监听器eventDispatcher:removeEventListener(self.backgroundListener)eventDispatcher:removeEventListener(self.foregroundListener)end```这样,当场景退出时,监听器被移除,就不会再调用已经销毁的self的方法。步骤3:如果问题依然存在,可能是由于Cocos2d-x3.10版本在iOS后台事件处理上有bug。可以尝试升级引擎版本,或者查找该版本是否有已知的类似问题。另外,参考引用[^2]中,iOS原生代码中设置了视频控件不响应事件,我们可以考虑在进入后台时,设置整个游戏不响应事件(但Cocos2d-x在进入后台时已经暂停了事件分发)。所以这一步可能不需要。总结:1.正确监听后台事件,并在进入后台时暂停游戏,回到前台时恢复。2.在节点退出时移除事件监听,避免回调时self失效。3.检查Cocos2d-x3.10版本是否存在已知的iOS后台事件处理bug。如果用户按照上述步骤处理,应该能够解决大部分问题。生成相关问题:1.Cocos2d-x中如何监听应用进入后台和返回前台的事件?2.Lua层如何避免回调函数中的self对象失效?3.Cocos2d-x3.10版本在iOS平台上有哪些已知的bug?</think>### Cocos2d-x 3.10 Lua iOS 后台事件异常与 self 对象问题分析 #### 问题原因分析 1. **iOS 生命周期事件未正确处理** 当应用切换到后台时,Cocos2d-x 会触发 `EVENT_COME_TO_BACKGROUND` 事件,但未正确处理可能导致 Lua 层事件紊乱。引用[3]中提到的 `CCLuaEngine` 初始化可能影响事件传递[^3]。 2. **self 对象失效问题** Lua 层回调函数中的 `self` 在后台切换时可能因对象被释放而失效,特别是通过 `tolua` 绑定的 C++ 对象。引用[4]表明绑定生成过程可能影响对象生命周期管理[^4]。 3. **渲染暂停与事件不同步** 进入后台时引擎自动暂停渲染(`Director::pause()`),但部分事件监听未同步暂停,导致事件队列堆积/紊乱。 --- #### 解决方案 ##### 步骤1:正确处理后台事件 在 `AppDelegate.cpp` 中确保事件转发到 Lua 层: ```cpp // AppDelegate.mm (iOS 实现) void AppDelegate::applicationDidEnterBackground() { Director::getInstance()->stopAnimation(); Director::getInstance()->getEventDispatcher() ->dispatchCustomEvent("APP_ENTER_BACKGROUND"); // 自定义事件名 } ``` 在 Lua 层监听事件: ```lua -- Lua 层事件监听 local function onEnterBackground() cc.Director:getInstance():pause() -- 暂停游戏逻辑 -- 暂停所有计时器、动画等 end local eventDispatcher = cc.Director:getInstance():getEventDispatcher() self.bgListener = cc.EventListenerCustom:create("APP_ENTER_BACKGROUND", onEnterBackground) eventDispatcher:addEventListenerWithFixedPriority(self.bgListener, 1) ``` ##### 步骤2:修复 self 对象失效 使用弱引用 + 有效性检查: ```lua function MyClass:registerEvents() -- 使用弱引用表存储 self local weakSelf = { __mode = "v", obj = self } local callback = function() local realSelf = weakSelf.obj if realSelf and not tolua.isnull(realSelf) then realSelf:doSomething() -- 安全调用 end end -- 绑定事件到安全的 callback end ``` ##### 步骤3:同步暂停/恢复逻辑 在 Lua 场景的 `onEnter`/`onExit` 中管理状态: ```lua function MyScene:onEnter() -- 监听恢复事件 self.fgListener = cc.EventListenerCustom:create("APP_ENTER_FOREGROUND", function() self:resumeLogic() end) end function MyScene:onExit() -- 移除所有后台相关监听器 eventDispatcher:removeEventListener(self.bgListener) eventDispatcher:removeEventListener(self.fgListener) end ``` --- #### 关键注意事项 1. **对象生命周期** 通过 `tolua.cast()` 检查 C++ 对象有效性: ```lua if tolua.isnull(self.nodeRef) then return -- 对象已销毁 end ``` 2. **iOS 原生交互** 引用[2]中 iOS 控件操作逻辑可借鉴,确保后台时禁用交互: ```objc // 在 UIVideoPlayer-ios.mm 中的类似逻辑 self.userInteractionEnabled = NO; // 禁止用户交互[^2] ``` 3. **绑定代码检查** 验证 Lua 绑定生成过程(引用[1][4]),确保 `lua_custom_api_auto.cpp` 中对象引用计数正确[^1][^4]。 --- ### 相关问题 1. Cocos2d-x 中如何正确管理 Lua 层对象的生命周期避免内存泄露? 2. iOS 后台切换时如何暂停 Cocos2d-x 的所有动画和计时器? 3. 在 Cocos2d-x Lua 绑定中,`tolua.isnull()` 和 `tolua.cast()` 的使用场景有何区别? [^1]: 生成成功后,在frameworks\cocos2d-x\cocos\scripting\lua-bindings\auto 中会找到我们生成的C++的桥接文件,lua_custom_api_auto.cpp 和 lua_custom_api_auto.hpp。 [^2]: 在ios版本里视频会有一个操作栏... self.moviePlayer.view.userInteractionEnabled = false; [^3]: Cocos2d-lua的运行理解。打开AppDelegate.cpp文件,看到引入的头文件... [^4]: Cocos2dx3.10 lua 绑定C++ 类(mac xcode)...修改frameworks/cocos2d-x/tools/bindings-generator/generator.py。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值