本篇包含如下干货

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