外观
Stage05|Performance
Lesson065|Event、Scheduler、闭包和监听器泄漏
Tags: #Creator2.x #Stage05 #Event #Scheduler #LeakDifficulty: ⭐⭐⭐⭐☆
真实问题:页面关闭了,为什么还在每秒刷新
页面节点已经销毁,但常驻 EventBus 和 Scheduler 仍保存回调:
text
Global EventBus
→ callback closure
→ PageComponent
→ Node / Sprite / Asset画面消失不等于引用链断开。回调既会重复执行,也可能让整棵对象图无法回收。
一、本课目标
有些内存泄漏不是资源 API 使用错误,而是“已经不用的对象仍然被某个回调系统持有”。本课重点:
- 事件监听器为什么会延长对象生命周期。
- schedule、Tween、Promise 和闭包如何保留引用。
- 如何建立成对的注册与清理规则。
二、引用链比画面更重要
对象不可见不代表可以回收:
text
EventBus
↓ listener
旧页面组件
↓ node
旧页面资源和子节点只要根部对象仍持有回调,整个对象图可能继续存活。
三、事件注册和取消必须成对
ts
onEnable() {
this.bus.on('data-change', this.refresh, this);
}
onDisable() {
this.bus.off('data-change', this.refresh, this);
}常见风险是:
ts
onLoad() {
this.bus.on('data-change', this.refresh, this);
}组件反复启用或场景切换时,如果没有明确取消,可能重复响应或无法回收。
四、匿名函数为什么难以取消
ts
this.node.on('touchend', () => this.fire(), this);如果没有保留同一个函数引用,之后很难准确 off:
ts
this.node.off('touchend', () => this.fire(), this);这两个箭头函数不是同一个对象。更稳妥的是使用成员方法或保存回调引用。
五、Scheduler 和 Tween 的残留
ts
this.schedule(this.refresh, 1);
cc.tween(this.node).to(1, { scale: 2 }).start();页面关闭、对象池回收或场景切换时,需要明确:
- 是否取消 schedule。
- 是否停止 Tween / Action。
- 回调是否检查对象仍有效。
- 回收后重新取出是否清空旧状态。
六、异步回调和 Promise
ts
loadData((data) => {
this.label.string = data.title;
});如果请求完成时页面已经销毁,回调可能访问无效对象。可以使用状态标记、请求令牌或生命周期解绑:
ts
private requestId = 0;
load() {
const id = ++this.requestId;
loadData((data) => {
if (id !== this.requestId || !this.node.isValid) {
return;
}
this.label.string = data.title;
});
}具体有效性 API 需按 Creator 2.x 版本确认,但原则是异步结果必须验证归属和生命周期。
七、案例:反复打开页面后的重复刷新
现象:
text
第一次打开:refresh 执行 1 次
第二次打开:refresh 执行 2 次
第三次打开:refresh 执行 3 次常见根因:每次打开都 on,但没有对应 off。排查方法:
- 统计注册次数。
- 给 listener 打印对象身份。
- 检查 onEnable/onDisable 是否对称。
- 场景切换后检查全局 Bus 是否仍持有旧对象。
八、泄漏和重复执行的区别
text
重复执行:同一个对象被多个 listener 或任务调用
内存泄漏:对象生命周期结束后仍被引用两者经常同时出现,但修复方式要分别验证:
- 执行次数是否恢复为预期。
- 旧对象是否能被回收。
- 场景切换后引用链是否断开。
九、常见误区
误区一:节点销毁后事件会自动从所有全局系统移除
不应假设所有自定义事件系统都能自动清理。
误区二:匿名函数方便,任何时候都能 off
没有同一个函数引用就无法准确取消。
误区三:Promise 完成后访问组件一定安全
异步完成时间可能晚于节点生命周期。
误区四:对象池回收后不用清理事件
复用对象会把旧 listener 带到下一次业务中。
十、练习与答案
- 为什么 onEnable/onDisable 适合管理事件?
- 匿名 listener 有什么取消风险?
- 异步回调为什么需要检查对象有效性?
- 如何区分重复执行和内存泄漏?
答案:
- 它们对应组件进入和退出可用状态,生命周期对称。
- 每次创建的匿名函数引用不同,无法准确 off。
- 回调完成时对象可能已关闭、销毁或被其他请求取代。
- 前者看调用次数,后者看生命周期结束后是否仍有引用。
监听器所有权表
每条监听应记录四项:
| 注册者 | 事件源 | 有效期 | 取消点 |
|---|---|---|---|
| Panel | Node touch | active 期间 | onDisable |
| BattleController | Network | battle 期间 | onDisable/onDestroy |
| GameRoot | App event | 应用期间 | shutdown |
| Pooled Item | EventBus | reuse 期间 | unuse |
没有取消点的监听就是设计缺口。
Creator 2.4.x 实验:复现重复回调
LeakProbe.ts
ts
const { ccclass } = cc._decorator;
const bus = new cc.EventTarget();
@ccclass
export default class LeakProbe extends cc.Component {
private tickCount = 0;
onEnable() {
bus.on('tick', this.onTick, this);
this.schedule(this.emitTick, 1);
}
onDisable() {
bus.off('tick', this.onTick, this);
this.unschedule(this.emitTick);
}
private emitTick = () => {
bus.emit('tick');
};
private onTick() {
this.tickCount++;
cc.log(
'[leak-probe]',
this.node.name,
this.tickCount
);
}
}实验
- 正常反复 active,确认每秒一次。
- 临时注释
off,反复五次,观察重复。 - 改成匿名函数注册/取消,观察函数身份问题。
- 创建多个实例并销毁,检查旧实例日志。
- 恢复正确代码并验证监听数稳定。
实验结束必须恢复清理代码。
闭包怎样延长对象生命
ts
const panel = this;
network.request(() => {
panel.refresh();
});回调捕获 panel。在请求完成或请求系统清除回调前,引用可能继续存在。
解决不只是加 cc.isValid:
- 若 API 支持取消,生命周期结束时取消;
- 回调使用 generation 拒绝过期结果;
- 请求系统完成后移除回调;
- 不在全局队列永久保存 Promise handler;
- 清除缓存 Map 中已结束任务。
真实项目故障:重连一次,网络消息多处理一遍
每次重连都执行:
ts
socket.onMessage((data) => this.handle(data));旧 socket handler 未移除,匿名函数又无法精确取消。第三次重连存在三套处理器。
修复:
- 保存稳定 handler;
- 关闭旧连接时解绑;
- 连接状态机保证只存在一个 active session;
- 消息增加 session ID,旧会话结果失效;
- 输出事件名、订阅数和 session。
泄漏与重复执行如何区分
重复执行是行为现象;泄漏是引用/内存现象。二者常同时出现,但也可能:
- 重复监听被 offAll 清掉,未长期泄漏;
- 缓存永久持有对象,却没有任何回调执行;
- Scheduler 重复运行但对象本就合法常驻。
需要分别给出调用次数与引用/内存证据。
版本边界与练习答案
Creator Node EventTarget、Scheduler、Tween 与第三方网络库取消 API 不同。Creator 2.4.x 项目应为外部系统封装一致生命周期。
- 为什么匿名函数难取消? 注册和取消创建的是不同函数对象。
- onDestroy 清理是否永远足够? 不够。inactive/入池对象还没 destroy,通常应在 onDisable/unuse 停止工作。
cc.isValid能释放闭包吗? 不能,只阻止使用无效对象;事件源仍可能持有回调。- 如何验证监听泄漏修复? 反复开关/重连,多次后监听数与单次回调数保持稳定,同时内存平台不阶梯增长。
十一、本课总结
text
事件、定时器、Tween 和异步回调都可能延长对象生命周期。
注册和清理要成对,异步结果要验证生命周期和请求归属。十二、下一课预告
text
Lesson066|load、preload、缓存与加载峰值