外观
Stage03|Runtime Objects & Assets
Lesson035|为什么 destroy 节点不等于释放资源?
Tags: #Creator2.x #Stage03 #Destroy #Release #Memory #AssetDifficulty: ⭐⭐⭐⭐☆
真实问题:关闭活动页后节点没了,内存为什么没降
活动页创建 300 个节点,加载 6 张大图。关闭时执行:
ts
panel.destroy();Hierarchy 中页面消失,但内存曲线几乎不动。开发者于是连续调用 releaseAsset,结果再次进入页面出现黑图。
这里混淆了三层生命周期:
text
Node/Component 对象生命周期
Asset 缓存与引用生命周期
Texture 等底层 CPU/GPU 内存生命周期destroy 只明确结束第一层,不会自动证明后两层已经无人使用。
一、本课目标
很多项目有这样的误判:
ts
node.destroy();然后认为图片、Prefab、Texture 和缓存都会马上释放。本课区分:
- Node 生命周期。
- Component 生命周期。
- 资源引用。
- Loader 缓存。
- GPU 资源释放。
二、destroy 作用在哪一层
text
node.destroy()
↓
Node 和挂载 Component 进入销毁流程
↓
节点树关系解除
↓
业务对象不再参与正常运行它主要处理运行时对象,不自动等价于:
text
删除磁盘资源
清空 Loader 缓存
释放所有 SpriteFrame
释放共享 Texture
释放 Bundle
立刻归还全部 GPU 内存三、节点引用和资源引用是两张图
text
Scene Node
↓ 使用
Sprite Component
↓ 引用
SpriteFrame
↓ 引用
Texture销毁 Node 后,第一条边可能解除,但其他对象和缓存仍可能保留后面的资源:
text
另一个 Sprite ─────→ Texture
Loader Cache ─────→ Texture因此资源是否释放要看整张依赖图。
四、destroy 后的延迟清理
Cocos 2.x 中,销毁通常不是在任意一行同步释放全部数据:
text
调用 destroy
↓
标记待销毁
↓
完成当前规则允许的调度
↓
引擎统一清理
↓
触发销毁生命周期业务上应该把对象视为不可继续使用,而不是利用延迟清理窗口继续访问。
五、资源释放需要什么条件
一个资源要安全释放,通常需要确认:
text
没有 Node / Component 使用
没有异步回调会继续使用
没有其他页面或系统引用
没有对象池保留
缓存策略允许释放
依赖资源也能安全处理这往往需要资源管理器或页面级资源上下文,而不是在某个 Node 的 onDestroy 中随意调用释放。
六、Creator 2.4 的动态资源引用计数
Creator 2.4.x 的关键边界是:把动态加载的资源赋给组件,不代表 Asset Manager 一定会自动记录这条运行时引用。 不能使用“Sprite 使用就自动 +1、销毁就自动 -1”的模型。
如果业务对象需要持续持有动态资源,应显式建立所有权:
ts
private iconFrame: cc.SpriteFrame = null;
loadIcon() {
cc.resources.load('ui/icon', cc.SpriteFrame, (err, frame) => {
if (err || !this.isValid) {
return;
}
if (this.iconFrame) {
this.sprite.spriteFrame = null;
this.iconFrame.decRef();
}
frame.addRef();
this.iconFrame = frame;
this.sprite.spriteFrame = frame;
});
}
onDestroy() {
this.sprite.spriteFrame = null;
if (this.iconFrame) {
this.iconFrame.decRef();
this.iconFrame = null;
}
}这里的语义是:
text
addRef:声明当前业务所有者需要继续持有 Asset
decRef:结束这份所有权,并让引擎按引用计数尝试自动释放
releaseAsset:确认具体 Asset 已无人使用时,将其从缓存释放
resources.release:按 resources 路径和类型释放资源cc.assetManager.releaseAsset(asset) 和 cc.resources.release(path, Type) 不能当作“无条件清内存”按钮;调用前仍要确认没有其他业务对象使用该资源。静态依赖和动态赋值的记录方式也不同。
官方版本依据:Creator 2.4 动态加载与资源释放。
七、案例:场景切换后内存没有下降
排查顺序:
text
1. 旧场景 Node 是否真的销毁?
2. 全局管理器是否仍引用旧节点?
3. 事件总线和定时器是否仍注册?
4. 资源是否被新场景共享?
5. Loader / Bundle 是否仍缓存?
6. GPU 是否等待后续帧或平台回收?
7. 采样工具看到的是 JS Heap 还是纹理内存?内存不下降不等于一定泄漏,也可能是合理缓存或平台延迟回收。
八、对象池和资源释放
对象池中的节点通常不 destroy:
text
使用结束
→ active=false
→ 回收到池
→ 仍然保留组件和资源引用这可以减少重新创建成本,但意味着池子会占用内存。对象池应设置:
- 最大容量。
- 空闲回收策略。
- 场景退出时的清理策略。
- 资源版本变化时的重建策略。
池化和释放是不同目标,不能同时无限追求。
九、常见误区
误区一:destroy 节点就会清空所有资源
节点对象和资源对象是不同生命周期。
误区二:调用 release 后 GPU 内存马上下降
缓存、依赖、渲染队列和平台回收时机都会影响观察结果。
误区三:内存没有下降就是泄漏
需要区分 JS Heap、Native 内存、纹理内存和缓存。
误区四:对象池越大越好
池子本身可能成为长期内存占用。
十、练习与答案
练习
- destroy 主要作用于哪一层?
- 为什么节点销毁后 Texture 仍可能存在?
- 如何排查场景切换后的内存问题?
- 对象池和资源释放的目标有什么冲突?
参考答案
- 运行时 Node 和 Component 对象生命周期。
- 其他节点或业务对象仍可能使用它,Asset Manager/Bundle 也可能仍缓存它;动态赋值还需要业务自己维护
addRef/decRef。 - 按节点、引用、事件、缓存、依赖、内存类型和回收时机分层检查。
- 对象池希望保留对象以减少创建,资源释放希望解除引用并降低占用。
一次正确关闭要经过哪些步骤
以动态活动页为例:
text
停止接收新输入
→ 取消事件、网络回调、定时器和 Tween
→ 使进行中的异步结果失效
→ 销毁或回收节点
→ 清除组件与缓存中的 Asset 引用
→ 由真正所有者归还 addRef
→ 等待引擎在合适时机处理依赖和底层资源
→ 观察多次循环后的稳定平台如果页面使用共享 UI 图集,它不能因为本页面关闭就释放;如果页面独占远程 Bundle,它又需要明确归还。
Creator 2.4.x 编辑器实验:拆开 Node 与 Asset
使用 Creator 编辑器创建实验 Prefab 和资源引用。不要直接修改
.prefab、.scene、.meta或生成目录。
准备
- 在
assets/resources/release-lab/导入一张图片。 - 创建一个显示该图的 Card Prefab。
- 创建空场景、Container 节点和控制按钮。
- 挂载
ReleaseProbe.ts。
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class ReleaseProbe extends cc.Component {
@property(cc.Node)
container: cc.Node = null;
private prefab: cc.Prefab = null;
private instance: cc.Node = null;
open() {
cc.resources.load(
'release-lab/card',
cc.Prefab,
(err, prefab: cc.Prefab) => {
if (err || !cc.isValid(this.node)) {
cc.error(err);
return;
}
if (!this.prefab) {
this.prefab = prefab;
this.prefab.addRef();
}
this.instance = cc.instantiate(prefab);
this.container.addChild(this.instance);
cc.log('[release] instance created');
}
);
}
destroyNodeOnly() {
if (this.instance && cc.isValid(this.instance)) {
this.instance.destroy();
}
this.instance = null;
cc.log(
'[release] node requested destroy',
'prefab owned=', !!this.prefab
);
}
releaseOwner() {
this.destroyNodeOnly();
if (this.prefab) {
this.prefab.decRef();
this.prefab = null;
}
cc.log('[release] owner released');
}
onDestroy() {
this.releaseOwner();
}
}实验 A:只销毁实例
打开 Card,再执行 destroyNodeOnly。预期节点进入销毁流程,但脚本仍明确持有 prefab。这已经足以证明“节点没了”不能推出“Prefab Asset 无人持有”。
实验 B:释放所有者
再次打开后执行 releaseOwner。预期脚本归还自己通过 addRef 声明的引用。不要额外多调用 decRef。
实验 C:帧末销毁
在同一函数中:
ts
const node = this.instance;
node.destroy();
cc.log('immediate valid=', cc.isValid(node));
this.scheduleOnce(() => {
cc.log('later valid=', cc.isValid(node));
}, 0);观察立即与稍后状态,理解 deferred destroy。具体日志以当前 2.4.x 小版本为准。
为什么系统内存不会立刻下降
即使逻辑释放正确,监控数值也可能不立即下降:
- JavaScript 堆等待下一次 GC;
- 原生分配器保留已释放内存供后续复用;
- GPU 驱动延迟销毁资源;
- Profiler 指标采样有延迟;
- 同一 Texture 仍被其他 Frame 使用;
- 场景切换瞬间新旧资源同时存在;
- 操作系统显示的是进程保留量,不是业务活跃量。
正确验证方式是重复稳定场景:
text
进入页面
→ 等待稳定
→ 记录峰值与稳定值
→ 关闭页面
→ 等待若干帧/合理时间
→ 再次进入
→ 重复 5~10 次若每轮稳定平台持续上升,才进一步寻找泄漏;如果首轮上升后复用并稳定,可能是缓存和分配器行为。
对象池为什么改变结论
放入对象池的节点没有被销毁:
text
Pool
└── inactive Card
└── Sprite → SpriteFrame → Texture只要池中节点保留 SpriteFrame,资源就仍有合法使用者。优化对象创建与释放资源是两个目标:
- 高频复用的小对象适合池化;
- 大资源或低频对象不应无限留池;
unuse要取消监听、定时器和异步结果;reuse要完整重置;- 对象池需要容量上限和清空时机。
真实项目故障:退出战斗后内存每局涨 20 MB
证据
节点数量回落,但动态纹理缓存中的 key 每局增加;资源路径带时间戳参数,导致相同皮肤被当作不同资源。
根因链
text
每局生成新 URL/path
→ Asset Manager 无法命中相同缓存键
→ BattleCache addRef
→ 退出只 destroy 节点
→ BattleCache 从不淘汰
→ 每局留下新 Texture修复
- 规范稳定资源键;
- 明确 BattleCache 的所有权范围;
- 战斗结束清除组件引用并归还缓存持有;
- 共享资源移交更长生命周期缓存;
- 连续运行十局验证稳定平台;
- 同时记录 JS、原生和纹理指标。
释放错误的反例
反例一:关闭任何页面都调用 releaseAll
它破坏其他页面共享资源,也让下次打开产生加载尖峰。
反例二:每个 Sprite 对自己的 Frame 调 decRef
组件可能不是 addRef 的所有者,共享 Frame 会被重复归还。
反例三:内存没降就连续 release
释放 API 不是强制 GC;重复归还会破坏引用协议。
反例四:对象池和 destroy 混用
一个对象既入池又 destroy,后续取出将访问无效节点。必须选择明确状态机。
版本边界与官方入口
本课以 Creator 2.4.x 的 deferred destroy 和 Asset Manager 引用计数为主。旧 loader 的释放接口、场景自动释放习惯与 2.4 Asset Manager 不应混用。
深化练习与答案
- destroy 一个引用图片的 Prefab 实例后,哪几类引用还可能存在? 其他实例/组件、业务缓存、Asset Manager 缓存、显式 addRef、SpriteFrame 对 Texture 的依赖以及对象池节点。
- 为什么关闭页面后应观察循环稳定平台? 单次数据受预热、GC、缓存和驱动延迟影响;多轮平台能区分一次性增长、可复用缓存和持续泄漏。
- 谁有资格调用 decRef? 当初通过 addRef 声明所有权、并能证明该所有权结束的模块。
- 对象池容量为什么属于内存策略? 池中节点及组件引用仍占用对象和资源;无上限池只是把销毁成本换成长期内存。
十一、本课总结
text
destroy 管对象,不会替你完成动态 Asset 的所有权管理。
Creator 2.4.x 的动态资源应明确使用 addRef / decRef 或受控的 release API。
资源释放必须确认所有使用者、静态依赖和缓存关系。
内存观察要区分 JS、Native、纹理和缓存。
对象池减少创建成本,但会长期占用资源。十二、下一课预告
text
Lesson036|阶段复习:从磁盘资源到运行时对象,再到安全释放