外观
Stage05|Performance
Lesson064|对象池解决了什么,又可能制造什么问题?
Tags: #Creator2.x #Stage05 #ObjectPool #Instantiate #DestroyDifficulty: ⭐⭐⭐☆☆
真实问题:用了对象池后,子弹为什么带着上一次的状态
回收后的子弹再次取出,出现:
- 旧颜色和伤害值;
- 上一次 Tween 继续播放;
- 命中事件执行两次;
- 异步特效回调落到新目标;
- SpriteFrame 一直占内存。
对象池只改变“销毁后重建”为“暂存后复用”,不会自动重置任何业务状态。
一、本课目标
对象池的核心是复用生命周期,而不是简单保存节点数组。本课说明:
- 什么时候适合对象池。
- 取出和回收时必须重置什么。
- 为什么对象池也可能导致内存和状态问题。
二、为什么频繁创建销毁会贵
text
instantiate
→ 创建 Node / Component
→ 恢复属性和引用
→ 执行生命周期
→ 进入调度和渲染
→ destroy
→ 清理和 GC子弹、飘字、特效和列表 Item 如果频繁经历这条链路,会产生 CPU、内存和 GC 压力。
三、对象池的基本模型
ts
const node = pool.get();
use(node);
pool.put(node);内部通常是:
text
预创建或按需创建
↓
空闲池
↓ get
激活并初始化
↓
使用
↓ put
取消任务、重置状态、隐藏
↓
回到空闲池四、回收时必须处理什么
text
active / enabled
Transform
颜色和透明度
动画和 Tween
schedule / Timer
事件监听
业务字段
子节点状态
外部引用如果只做:
ts
node.active = false;旧的 Tween、事件和数据可能在下次取出时继续生效。
五、取出时的初始化
建议提供显式接口:
ts
spawn(data: DamageData) {
this.node.active = true;
this.resetVisualState();
this.setData(data);
this.startAnimation();
}
despawn() {
this.stopAnimation();
this.unscheduleAllCallbacks();
this.node.active = false;
}具体方法需按项目组件结构实现,但“取出初始化、回收清理”必须对称。
六、池容量和峰值
对象池应有容量策略:
text
最小预热数量
最大容量
池满时如何处理
长期不用时是否缩容
场景切换时是否销毁无限增长的对象池只是把运行时创建问题换成了内存问题。
七、哪些对象不适合池化
不一定适合:
- 生命周期很长的唯一对象。
- 状态复杂且难以完整重置的对象。
- 创建成本很低、出现频率很低的对象。
- 需要严格资源释放的临时对象。
对象池有维护成本,必须用采样证明收益。
八、案例:子弹池
text
发射 → get
飞行 → update
命中或超时 → put回收前至少要:
- 停止速度和轨迹。
- 取消命中回调。
- 停止粒子和音效引用。
- 重置位置、旋转、透明度。
- 从碰撞或事件系统移除。
否则下一次取出可能出现“上一颗子弹的状态”。
九、常见误区
误区一:对象池越大越好
池过大浪费内存,池过小仍频繁创建,需要根据峰值和设备测量。
误区二:回收就是 active=false
还需要清理事件、Tween、定时器和业务状态。
误区三:所有对象都应该池化
维护复杂度可能超过创建成本。
误区四:池对象不能 destroy
达到淘汰条件、切场景或资源更新时,池也需要释放对象。
十、练习与答案
- 对象池主要减少什么成本?
- 回收时为什么要停止 Tween 和 schedule?
- 无限增长的对象池有什么问题?
- 如何判断一个对象是否值得池化?
答案:
- instantiate、destroy、生命周期、内存分配和 GC 成本。
- 防止对象回收后旧任务在下一次复用时继续修改它。
- 长期占用内存并可能掩盖真实峰值。
- 看出现频率、创建成本、峰值、复用次数和重置复杂度。
池对象需要明确状态机
text
Created
→ Active
→ Returning
→ Pooled
→ Reusing
→ Active
→ Destroyed(池清空)每次转换都要定义:
- 是否在节点树;
- active 状态;
- 事件是否注册;
- Scheduler/Tween 是否运行;
- 异步 generation;
- 资源引用;
- 业务数据;
- 物理/碰撞状态。
Creator 2.4.x 编辑器实验:有缺陷与完整 reset
Bullet.ts
ts
const { ccclass } = cc._decorator;
@ccclass
export default class Bullet extends cc.Component {
private version = 0;
private damage = 0;
reuse(damage: number) {
this.version++;
this.damage = damage;
this.node.active = true;
this.node.opacity = 255;
this.node.color = cc.Color.WHITE;
this.node.setPosition(cc.v2());
this.node.on('hit', this.onHit, this);
}
unuse() {
this.version++;
this.node.off('hit', this.onHit, this);
this.unscheduleAllCallbacks();
cc.Tween.stopAllByTarget(this.node);
this.damage = 0;
this.node.active = false;
}
private onHit() {
cc.log('[bullet] damage=', this.damage);
}
}Tween API 以项目 2.4.x 小版本为准;若使用第三方 Tween,要调用其对应停止接口。
PoolProbe.ts
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class PoolProbe extends cc.Component {
@property(cc.Prefab)
bulletPrefab: cc.Prefab = null;
@property(cc.Node)
container: cc.Node = null;
private pool = new cc.NodePool();
spawn() {
const node = this.pool.size() > 0
? this.pool.get()
: cc.instantiate(this.bulletPrefab);
this.container.addChild(node);
node.getComponent(Bullet).reuse(10);
}
recycle(node: cc.Node) {
node.getComponent(Bullet).unuse();
this.pool.put(node);
}
onDestroy() {
this.pool.clear();
}
}注意 cc.NodePool.get/put 可配合组件的 reuse/unuse 协议;不要在项目里同时手动调用与自动协议造成双重重置。以上示例表达责任,实际写法按项目选择一种。
实验
- 故意不 off,复用五次,观察重复命中。
- 不停止 Tween,观察旧动画。
- 不重置颜色/位置,观察脏状态。
- 完整 reset 后连续复用。
- 对比 instantiate/destroy 与池化的 p99、内存和稳定 CPU。
池容量如何决定
text
容量下限:常见并发需求
容量上限:可接受常驻内存
超出策略:销毁、延迟回收或扩容
缩容时机:场景结束、内存警告、长期空闲不要直接用历史峰值永久预热。极端峰值可能只发生一次。
真实项目故障:弹幕池预热导致启动变慢
团队启动时预创建 5000 个弹幕,每个含 Label、碰撞器和 Sprite。战斗很顺,但启动增加数秒、内存常驻高。
修复:
- 只预热首秒合理数量;
- 后续分帧扩容;
- 弹幕数据与显示节点分离;
- 简化单个弹幕 Component;
- 设置上限与溢出降级;
- 场景结束缩容。
版本边界与练习答案
Creator 2.4.x cc.NodePool 与组件 reuse/unuse 的具体调用规则需以 API 为准。项目应封装单一池接口。
- 对象池主要解决什么? 高频创建/销毁和相关生命周期尖峰。
- 为什么池会保留资源? 池中节点及 Sprite/Component 字段仍引用 Asset。
- unuse 必须做什么? 取消监听/定时器/Tween/异步结果,清业务数据和外部引用,恢复可复用状态。
- 如何证明池有效? 对比目标动作 p99、创建/GC、稳定内存与正确性,而非只看“没有 instantiate”。
十一、本课总结
text
对象池复用的是对象生命周期。
get/put 必须配套初始化和清理。
池容量、状态重置和资源释放决定对象池是否真的有效。十二、下一课预告
text
Lesson065|Event、Scheduler、闭包和监听器泄漏