这周 @bin 告诉我项目有比较严重的内存泄露, 任意一个界面不停的打开关闭, 内存占用会一直往上涨, 直到被系统 kill 掉.
一. 确定问题
收到问题后, 我简单写了一段测试代码, 加载/移除界面 100 次, 对比内存变化:
1 | local index = 0 |
为了方便, 我没有进行真机测试, 而是使用 xcode 启动 player 来进行测试, 在 Xcode 的 Debug Navigator/Memory Report
窗口查看结果.
在测试前的平稳内存为 194M , 测试后惊人的达到了 258M, 十分严重的内存泄露了!
二. 初步解决问题
由于项目是纯 lua 的, 所以不太可能是数据和逻辑的问题, 那么很有可能是视图(cocos2d-x 对象)存在内存泄露. 而每一个界面都存在问题, 那么很可能是某个通用组件存在问题.
经过一番努力, 最终成功找到了内存持续增加的原因, 一共两处:
1. lua 垃圾没有及时回收
lua 的垃圾是会自动回收的, 但我们有时候可能需要手动回收下, 比如切换场景时, 关闭界面时, 主动回收的代码很简单:
1 | collectgarbage("collect") |
我将这段代码加到了统一关闭界面的地方.
更多关于 lua 垃圾回收的具体问题大家可以参考这个两篇文章:
http://luatut.com/collectgarbage.html
http://www.codingnow.com/2000/download/lua_manual.html
2. 精灵变灰和高亮的 shader 创建后一直没有释放
1 | function GameUtils.SetSpriteGrey(sprite,is_grey) |
这段代码看起来没有任何问题, 我能想到只是没有用 GLProgramCache
缓存起来, 造成每次都会创建的效率问题, 应该不会导致泄露吗 ? 精灵被释放时难道不会自动释放所引用的 GLProgram 吗?
因为当时项目比较紧急, 我将这段代码使用 GLProgramCache
的形式修改了一下, 惊奇的发现内存泄露问题竟然解决了. 修改后的代码如下:
1 | function GameUtils.SetSpriteGrey(sprite,is_grey) |
三. 隐藏在背后的秘密
问题解决了, 那么是不是可以结束了呢 ? 并不能, 晚上回到家我就一直在思考这个问题, 倒是是什么原因导致了 GLProgram 内存没有释放, 而使用 GLProgramCache 就没有问题.
莫不是 cocos2d-x 的 bug ? cocos 对象是基于引用计数去自动释放内存的. 我排查了几处可疑的地方:
- GLProgram::createWithByteArrays 调用了 autorelease
- Node::setGLProgram 调用了 retain
- Node::~Node 调用了 release
貌似都没有问题. 那么我可以跟踪下引用计数的变化, 看看是哪一步出现的问题! 经过一番调试,最终定位到了问题, 大家请看:
1.Node::setGLProgram
进入到 GLProgramState::getOrCreateWithGLProgram
, 这一步没有什么问题.
2.这里有一个新的缓存 GLProgramStateCache
, 进入它到 getGLProgramState
函数.
3.这一步是 GLProgramStateCache 的核心代码了, 判断有无在缓存中, 没有则 insert 到末尾. 这里的 _glProgramStates
是一个 Map
, 而这个 map 竟然是以传递进来的 glprogram
做
key , 而我们每次传递进来的 glprogram 都是新创建的, 所以在我们这个使用情况下缓存根本是无效的.
4.让我看一下, GLProgramState
的 init
函数, 这下找到 retain 地方了.
这样的话, 就讲得通了, GLProgram 会在 GLProgramState 析构的时候 release 掉. 而 GLProgramState 只会在 GLProgramStateCache:removeAllGLProgramState 释放掉. 而 removeAllGLProgramState 只有在手动或者游戏退出的时候才会调用.
OK, 这下定位到了问题, 虽然我们使用有些问题, 但 GLProgramStateCache 设计确实有不合理的地方, 大家记得正确用法就好了, 就是 shader 一定要使用 GLProgramCache !
后记
通过这次解决问题, 我有一个特别大的收获. 就是做优化工作时一定不能去猜, 要有数据和逻辑的支持.