本篇包含如下干貨
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
