
微信小游戏里的内存泄漏,藏在这3个“隐形角落”里
很多开发者觉得“内存泄漏是高端问题”,其实它常躲在你习以为常的代码里。我帮10多个小游戏调优过,发现90%的泄漏都逃不出这3种情况:
全局变量是内存泄漏的“重灾区”——因为全局变量会被浏览器一直“抱着”,除非手动销毁,否则永远不会被垃圾回收器回收。我朋友那游戏里,有个全局数组叫allBlocks
,用来存所有方块对象,每次消除方块时,只从画面上移除了,但没从数组里删掉,结果玩10分钟,数组里就堆了5000个无用对象,占了150M内存。
你可能会说“我没写全局变量啊”?其实很多时候是“不小心”——比如用var
声明的变量(不是let
或const
)、把变量挂在window
对象上(比如window.userInfo
),甚至在函数里漏写let
,都会变成全局变量。比如这段代码:
// 错误写法:漏写let,变成全局变量
function createBlock() {
block = new Block(); // 没写let,block成了全局变量
allBlocks.push(block);
}
这样的变量,就算你把block
从画面上删了,它还在全局里“占着坑”。
微信小游戏里的事件监听,比如wx.onTouchStart
、canvas.addEventListener
,就像“贴在对象上的便利贴”——如果对象销毁时没把便利贴“撕下来”,就算对象没了,监听还在,内存也没法回收。
我之前帮一个跑酷游戏调优时,发现他给角色加了touchmove
事件监听,但角色死亡后没调用off
方法,结果每生成一个新角色,就多一个监听,玩20分钟,内存里堆了30个没用的监听对象,占了80M内存。微信开放社区的文档里特意提过:“所有addEventListener的事件,都要对应写removeEventListener”,就是怕这种情况。
循环引用是更“隐蔽”的泄漏——比如玩家对象player
引用了武器对象weapon
,而weapon
又引用了player
,这样就算你想销毁player
,因为weapon
还抱着它,垃圾回收器没法把它们收走。
我碰到过一个格斗游戏,角色对象fighter
里有个weapon
属性,武器对象weapon
里又有个owner
属性指向fighter
,结果角色死亡后,fighter
和weapon
都没法被回收,每局游戏结束,内存就涨10M。后来改成弱引用(用WeakMap
或WeakSet
),问题就解决了——弱引用不会“抱着”对象,只要对象没人用了,垃圾回收器就能收走。
我把这些常见泄漏和解决方法整理成了表格,你可以直接对照着查:
泄漏类型 | 常见场景 | 检测技巧 | 解决方法 |
---|---|---|---|
全局变量 | 用var声明变量、挂在window上的变量 | 看全局对象(window)的属性列表 | 用let/const代替var,不用全局变量 |
事件监听 | 角色/道具销毁时没removeEventListener | 查事件监听列表(Chrome DevTools的Event Listeners面板) | 对象销毁时调用off/removeEventListener |
循环引用 | 玩家→武器→玩家的互相引用 | 看对象的引用链(Dominator树) | 用WeakMap/WeakSet代替强引用 |
用微信开发者工具抓泄漏,我亲测有效的3步“笨办法”
知道了泄漏的原因,接下来就是“抓现行”——微信开发者工具自带的内存面板,其实是个“泄漏侦探”,我用它找出过80%的泄漏问题,关键是要会“用对步骤”。我把自己的方法 成了3步,就算你是新手,也能跟着做:
打开微信开发者工具,点左边的调试器→Memory标签(如果没看到,点右上角的“+”添加)。内存面板里有3个功能:Heap快照、Allocation instrumentation on timeline、Allocation sampling——优先用Heap快照,因为它最简单直接,就像“给内存拍张照片”,能看到某个时刻所有存在的对象。
点击“Take Heap Snapshot”按钮,等个几秒钟,就能生成一张快照。快照的名字会显示当前时间,比如“Heap Snapshot 1 (100M)”,100M是当前内存占用。
光拍一张快照没用,得拍两张对比——比如:
然后在内存面板的下拉框里选Comparison(对比模式),把“后续快照”和“初始快照”对比,就能看到“新增的对象”——如果某个对象类型的数量或大小大幅增加,比如Block
对象从100个涨到5000个,那大概率是泄漏了。
比如我朋友那游戏,对比后发现Block
对象新增了4900个,点进去看引用链,发现都是allBlocks
数组里的无用对象,一删就解决了。
找到新增对象后,下一步是“看谁占的内存多”——点击内存面板里的Dominator Tree(支配树)标签,按Retained Size(保留大小)排序(从大到小)。Retained Size是“这个对象及其引用的对象总共占的内存”,比如一个ParticleSystem
对象的Retained Size是20M,说明它和它引用的粒子占了20M内存。
我之前帮跑酷游戏调优时,支配树里排第一的是ParticleSystem
对象,Retained Size20M,点进去看引用链,发现是角色销毁时没调用destroy()
方法,导致粒子系统一直存在。改成“角色死亡时调用particleSystem.destroy()
”后,这个对象的Retained Size变成了0,卡顿率从15%降到了3%。
我之前帮过的小游戏开发者里,有个做塔防游戏的,按这3步找到一个没被销毁的怪物对象,占了30%内存,解决后,用户闪退率从20%降到了5%,留存率涨了12%。其实内存泄漏这事,没想象中难——关键是要“看见”它,用对工具,找对地方。
你要是按这些方法试了,不管成功还是遇到问题,都可以来评论区跟我聊聊——比如你拍了快照没找到泄漏点,或者对比后发现新增对象太多看不懂,我帮你看看。毕竟我踩过的坑,不想让你再踩一遍。
先给你说个最常见的循环引用场景——玩家和武器。你想啊,玩家对象得握着武器对吧?所以写player.weapon = weapon;可武器也得知道自己属于哪个玩家吧?于是又加了weapon.owner = player。这俩就跟拔河似的,互相抱着不放了——哪怕玩家死了、武器从画面上消失了,只要这俩引用还在,垃圾回收器就不敢动它们,内存就跟被占了坑似的,漏在那越来越多。
这时候WeakMap就像个“懂分寸的中间人”。你不用直接给武器或者玩家加属性,而是建个WeakMap专门存“武器→玩家”的关系。比如先写const weaponOwner = new WeakMap(),等要给武器分配玩家的时候,不用weapon.owner = player,而是用weaponOwner.set(weapon, player)。哎你看,这样一来,武器和玩家之间没有直接的“强绑定”了——WeakMap里的引用是“弱”的,就像用橡皮筋轻轻拴着,不是铁链子。垃圾回收器会盯着,如果除了这个WeakMap,没有其他地方再用这俩对象了(比如全局变量没挂着它们,或者其他函数也没引用),那回收器就敢把这俩都收走,不会因为互相引用就卡着不动。
再说说WeakSet,它适合存那种“只需要知道‘在不在’,不用挨个遍历”的对象集合。比如你做了个道具系统,想存所有待销毁的道具,要是用普通Set,存进去的道具哪怕已经没用了,Set也会攥着它们不让回收——就像你把垃圾放进抽屉,抽屉没扔,垃圾就一直在。但用WeakSet就不一样,比如建个const toDestroyItems = new WeakSet(),把要销毁的道具加进去(toDestroyItems.add(item)),等道具真的没人用了,不管WeakSet里有没有它,垃圾回收器都能直接收走。我之前帮朋友调一个冒险游戏的内存泄漏,他之前用Set存临时怪物,结果每局结束内存都降不下来,换成WeakSet之后,每局结束内存都能回落到初始的80%左右,特省心。而且WeakSet不用你手动删元素,它自己会跟着垃圾回收走,省了好多清理代码。
微信小游戏里怎么快速判断有没有内存泄漏?
最直接的办法是用微信开发者工具的内存面板拍两张Heap快照对比。比如游戏刚启动时拍一张“初始快照”(记录初始内存状态),玩5-10分钟(反复进关卡、切场景)再拍一张“后续快照”,然后切换到Comparison(对比)模式,查看两张快照的新增对象——如果某个对象类型(比如Block、Player)的数量或大小大幅增加(比如从100个涨到5000个),大概率是内存泄漏了。
全局变量导致的泄漏,除了不用var还有什么办法?
除了用let/const代替var、不把变量挂在window对象上,还要注意“手动清理”全局变量。比如全局数组allBlocks,每次消除方块时,不仅要从画面移除,还要用splice或filter删掉数组里的对应元素(比如allBlocks.splice(index, 1));如果是全局对象(比如window.userInfo),不用时可以赋值为null(window.userInfo = null),让垃圾回收器能识别并回收这些“无用对象”。
事件监听一定要手动移除吗?有没有更省心的方式?
如果觉得手动写removeEventListener麻烦,可以试试这两个办法:
WeakMap/WeakSet怎么解决循环引用?能举个简单例子吗?
比如玩家(player)和武器(weapon)互相引用的问题,可以用WeakMap存弱引用。比如:
这样即使player和weapon互相引用,只要没有其他强引用(比如全局变量),垃圾回收器就能回收它们。WeakSet同理,适合存不需要遍历的对象集合(比如存所有待销毁的道具),不会阻止垃圾回收。