Memory error with schedule

First, please run below code:

sceneGame = cocos2d.CCScene:node()

icons = {}

function validateIcon(icon)
    if icon._isUsed then
        print("USED ICON:", icon)
    else
        print("      NEW:", icon)
        icon._isUsed = true
    end
end

-- create icons, append to scene
function createIcons(num)
    for i = 1, num or 30 do
        local icon = cocos2d.CCSprite:spriteWithFile("CloseNormal.png")
        icon:setPosition(cocos2d.CCPoint(math.random(20, 460), math.random(-350, -30)))
        validateIcon(icon)
        icons[#icons + 1] = icon
        sceneGame:addChild(icon)
    end
end

function tick()
    for i = #icons, 1, -1 do
        local icon = icons[i]
        local pos = icon:getPosition()
        pos.y = pos.y + 3
        if pos.y > 340 then
            sceneGame:removeChild(icon, false)
            table.remove(icons, i)
            createIcons(1)
        else
            icon:setPosition(pos)
        end
    end
end

createIcons()

cocos2d.CCScheduler:sharedScheduler():scheduleScriptFunc("tick", 1.0 / 60, false)
cocos2d.CCDirector:sharedDirector():runWithScene(sceneGame)

Check debug output:

      NEW:  userdata: 0x5e48ba4
      NEW:  userdata: 0x5e4b034
      NEW:  userdata: 0x5e4abc4
      NEW:  userdata: 0x5e4b354
      NEW:  userdata: 0x5e4b014 <---
      NEW:  userdata: 0x5e4adc4
      NEW:  userdata: 0x5e4c394
      NEW:  userdata: 0x5e4c824
      NEW:  userdata: 0x5e4cbe4
      NEW:  userdata: 0x5e4bdf4
      NEW:  userdata: 0x5e4d8e4
      NEW:  userdata: 0x5e4dd64 <---
      NEW:  userdata: 0x5e4e1f4
      NEW:  userdata: 0x5e4e494
      NEW:  userdata: 0x5e47e74
      NEW:  userdata: 0x5e4ef64
      NEW:  userdata: 0x5e4f3f4
      NEW:  userdata: 0x5e4f774
      NEW:  userdata: 0x5e4d224
      NEW:  userdata: 0x5e4fe94
      NEW:  userdata: 0x5e50324
      NEW:  userdata: 0x5e507b4
      NEW:  userdata: 0x5e4d314
      NEW:  userdata: 0x5e4d484 <---
      NEW:  userdata: 0x5e50924
      NEW:  userdata: 0x5e519b4
      NEW:  userdata: 0x5e51e44
      NEW:  userdata: 0x5e522b4
      NEW:  userdata: 0x5e52744 <---
      NEW:  userdata: 0x5e529f4
      NEW:  userdata: 0x5855574
USED ICON:  userdata: 0x5e4dd64
USED ICON:  userdata: 0x5e4b014
USED ICON:  userdata: 0x5e52744
USED ICON:  userdata: 0x5e4d484

OK, problems found. “icon” objects reuse same memory address.

  1. 从 C*+ 构造一个 CCSprite 对象(占用内存地址 MEM1),然后封装为 Lua_userdata 赋值给 a
    local sprite = CCSprite:spriteWithFile

  2. 调用 removeSelf 方法,会导致 C*+ 端的对象被 delete。但 Lua 端的 sprite 值仍然存在。

sprite:removeSelf()

  1. 当再一次调用 CCSprite:spriteWithFile(…) 时,如果 sprite 值还未被垃圾回收,并且新建 CCSprite 对象使用了先前用过的 MEM1 内存地址。那么新的 sprite 仍然指向前一个 sprite 的 metatable。

终于找出了问题的根源 :frowning:

复述一下 cocos2d-x 执行 lua 的步骤:

  1. 初始化 cocos2d-x

  2. 载入 hello.lua 并执行

  3. hello.lua 中用 scheduleScriptFunc() 创建了一个定时器事件

  4. 当 cocos2d-x 执行 void CCDisplayLinkDirector::mainLoop(void) 时,内部会执行下列代码:

    drawScene();
    CCPoolManager::getInstance()->pop();

drawScene() 会绘制图像,并调用 CCScheduler 触发计时器事件。CCScheduler 则会调用 scheduleScriptFunc() 注册的脚本函数。
CCPoolManager::getInstance()->pop() 会调用 CCAutoreleasePool::clear() 来释放内存。

问题在于 CCAutoreleasePool::clear 直接 obj->m_bManaged = false,导致 obj 实际上就脱离了 AutoreleasePool 的管理。

由于 obj 脱离了 AutoreleasePool 的管理,所以在 lua tick() 函数中的 sceneGame:removeChild(icon) 会导致 icon 对应的 c*+ 对象被立即 delete。此时 Lua 的状态机就出现了混乱,因为 lua 中 icon 值对应的 c*+ 对象已经不存在了。

如果此时再创建新的 c*+ 对象(createIcons函数),并且正好用到了刚刚 delete 释放的内存块,那么 Lua 得到的值就是错误的。
—————
这个问题还和 lua 的执行机制有关系。
当 cocos2d-x 用 lua_dofile 执行 hello.lua 时,实际上是把 hello.lua 整个封装为一个匿名函数来执行。执行完成后控制权交回给 cocos2d-x。
这时 cocos2d-x 就会调用 CCAutoreleasePool::clear,从而导致 hello.lua 中创建的 icon 对应的 c*+ 对象脱离 AutoreleasePool 的管理。

当第二次执行 drawScene() 时,会触发 hello.lua 中注册的 tick() 函数。但由于此时先前创建的 icon 图片对象都已经脱离了 AutoreleasePool 管理,所以 sceneGame:removeChild(icon) 的结果就是立即 delete c++ 对象。

我另外写了两段代码演示这个问题。

嗯,问题不在 obj->m_bManaged = false 那个地方,但根源还是自动释放池。

执行 lua_dofile(hello.lua) 时,hello.lua 中创建了一些 CCSprite 对象。当 lua_dofile() 执行完毕后,CCAutoreleasePool::clear() 又将这些对象从 m_pManagedObjectArray 里面移除了,减少了 这些对象的引用计数。下一轮 CCDirector:drawScene() 时,调用了 lua 中的 tick() 方法。此时 removeChild() 就会导致被移除对象的引用计数变成0,从而立即释放掉。

如果lua创建对象和removeChild() 都是在 CCAutoreleasePool::clear() 之前进行,那就不会有问题,因为 lua 里调用 removeChild() 后,对象的引用计数还大于0(被 m_pManagedObjectArray 保持),所以不会破坏 lua 值和 c++ 对象之间的对应关系。

又做了一些测试,此问题看来很难解决了。cocos2d-x 从设计上最初没有考虑 lua,后来添加 lua 支持就存在一些问题。

最基本的就是无法解决 lua userdata <-> c*+ object 之间的关系维护。如果 c*+ object 被删除了,持有这个 c++ object 指针的 lua userdata 没有被清理掉。

继续折腾,最后得出结论:此问题无解。

研究了一下 MOAI 的源代码,发现 MOAI 是从底层设计上就把 C++ object 和 lua value 关联起来了,所以不存在此问题。

郁闷啊。。。。。

大牛,那样用cocos lua写的游戏运行结果岂不是都有出现不可预期的概率?之前我也意识到了在cocos 里lua代码里的引用不能阻止userdata 对象的被回收,所以对象的缓存比较容易出错,但你反应的问题就更严重了,这样写出来的游戏怎么发布,有什么解决妙招吗?