本篇包含如下干货
1.JavaScript垃圾回收机制理解
2.CocosCreator内存泄漏排查与管理
3.Chrome内存调试技巧
转载请注明原文地址
http://www.cnblogs.com/billyrun/articles/7257742.html
JavaScript垃圾回收机制理解
js内存策略主要有标记清除和引用计数两种
具体由浏览器实现
简单来讲如果有全局变量
window.val = [...]//10000个数字的数组
显然这时val要占用一部分内存
若重新赋值
window.val = null
在仅有以上两条语句的情况下
之前的数组会被清除掉,所占用的内存会自动回收
回收不是立刻进行的,但一般来讲也很快
函数中的局部变量在函数结束之后,也会进入自动回收队列
所以通常js开发只需要关心全局变量
并不需要过多担心内存问题
当然,结合了游戏引擎之后就不一样了!以下会结合例子说明
CocosCreator内存泄漏排查与管理
在内存管理方面
CocosCreator开发H5游戏与原生游戏可以说完全不同
H5游戏使用js自身的内存管理策略,就是上文所说的
而原生平台使用jsb技术依靠cpp层面的引用记数来管理节点、纹理等内存,和2dx时代一样
区别有多明显呢?
比如H5版本中
window.node = new cc.Node()
那边全局变量node会一直可用,占用内存,直到手动destroy为止
而2dx-lua中
node = CCNode:create()
同样是全局变量,但由于create方法设置了autoRelease
若不增加引用记数(比如加入场景),那么node只能'存活'一帧的时间,下一帧就被释放掉了
在H5游戏开发中,绝大多数节点创建时作为局部变量
若未加入场景,则上下文结束后,标记清除等待回收
若加入了场景,实际上全局场景队列会保留其引用,因此不会被清除
若游戏内设置全局变量保存某节点,且其parent==null,此节点也会常驻内存,可以理解为用户有意保留不做销毁
内存问题举例
开发过程中遇到了一个造成内存严重泄漏的bug
按钮由工厂方法创建,若未加入场景,导致内存泄漏
与上文节点的创建不同之处在于,按钮注册了on('click',...)或on('touchend'...)回调函数
因此其引用被保存在cc.eventManager全局变量中
下面通过具体的调试来论证这一点
介绍排查内存泄漏问题的基本方法
ps.若单纯创建节点,不加入场景也不注册点击事件,那么该节点或精灵是可以被自动回收的
Chrome内存调试技巧
首先升级Chrome至最新版本(本文使用59.0.3071.115)
然后打开'开发者工具' 选择Memory页签 选择Take heap snapshot
可以看到出示内存19.2MB
接下来我们创建5000个按钮节点
不加入场景也不保存引用
可以看到,我们只为按钮注册了touchend事件
并未保存引用或加入场景,然而页面内存激增至32.1MB
原因就在于cc.eventManager全局变量保留了每一个按钮节点的引用
导致按钮节点不会自动回收
引擎源码查考
CCNode:on方法有这样一段
this._touchListener = cc.EventListener.create({ event: cc.EventListener.TOUCH_ONE_BY_ONE, swallowTouches: true, owner: this, mask: _searchMaskInParent(this), onTouchBegan: _touchStartHandler, onTouchMoved: _touchMoveHandler, onTouchEnded: _touchEndHandler }); if (CC_JSB) { this._touchListener.retain(); } cc.eventManager.addListener(this._touchListener, this); newAdded = true;
注意owner就是按钮节点
该引用保存在_touchListener中并被加入cc.eventManager
又经过CCEventManager:_forceAddEventListener加入cc.eventManager._listenersMap
_forceAddEventListener: function (listener) { var listenerID = listener._getListenerID(); var listeners = this._listenersMap[listenerID]; if (!listeners) { listeners = new _EventListenerVector(); this._listenersMap[listenerID] = listeners; } listeners.push(listener); if (listener._getFixedPriority() === 0) { this._setDirty(listenerID, this.DIRTY_SCENE_GRAPH_PRIORITY); var node = listener._getSceneGraphPriority(); if (node === null) cc.logID(3507); this._associateNodeAndEventListener(node, listener); if (node.isRunning()) this.resumeTarget(node); } else this._setDirty(listenerID, this.DIRTY_FIXED_PRIORITY); },
调用_associateNodeAndEventListener时又加入cc.eventManager._nodeListenersMap
_associateNodeAndEventListener: function (node, listener) { var listeners = this._nodeListenersMap[node.__instanceId]; if (!listeners) { listeners = []; this._nodeListenersMap[node.__instanceId] = listeners; } listeners.push(listener); },
内存分析图解
了解了代码来龙去脉之后
我们从内存分析的视角来找问题
从上图所示占有内存最多的Object着手
可以看到其中一个疑似泄漏内存对象的引用如下图
正是通过_forceAddEventListener加入的_nodeListenersMap和_listenersMap
在分别打开可以看到引用具体所在信息
_nodeListenersMap和_listenersMap都可以找到owner
即5000个之中的按钮节点
再详细查其实可以确定其instanceID与我们生成时是一致的
与查看源码时获得的信息一致
关掉Object打开cc_Node
找到疑似问题节点
同样可以看到其引用关系
验证的最后一步
我们在console中输入以下语句
清除我们刚刚发现的引用
cc.eventManager._listenersMap.__cc_touch_one_by_one._sceneGraphListeners = {}
cc.eventManager._nodeListenersMap = {}
再次计算内存,发现内存回到初始值,5000个按钮节点被释放回收!
总结
遇到具体内存泄漏问题时
往往是从开发者工具Memory反应的信息着手倒推
找到问题代码出现的源头
这次内存调试,发现了我们程序代码中的写法错误
杜绝类似'创建按钮后不使用'这样的行为之后
游戏的内存状况得到了大大改善
此外还有一点dragonBones使用的小经验
dragonBones.CCFactory.getFactory().clear()
db会cache许多动画数据信息甚至可以多至近百兆
及时清理也可以解决内存不足问题
参考文献
http://www.cnblogs.com/mizzle/archive/2011/08/12/2135838.html