外观
Stage05|Performance
Lesson067|资源释放、场景切换与内存不下降
Tags: #Creator2.x #Stage05 #Asset #Release #Scene #MemoryDifficulty: ⭐⭐⭐⭐☆
真实问题:场景 A 已销毁,为什么内存曲线不回到启动值
切到 B 后,A 的 Hierarchy 消失,但内存只下降一部分。团队立即判断“引擎泄漏”,却没有检查:
text
常驻节点
共享资源
Asset 缓存/addRef
对象池
异步回调
GPU 延迟
分配器保留
B 场景新资源场景切换是新旧生命周期交叠,不是内存清零按钮。
一、本课目标
本课建立一个重要判断:
destroy 节点、解除引用、释放资源、归还进程内存,不是同一件事。
二、对象、资源和缓存的关系
text
Scene
→ Node / Component
→ SpriteFrame
→ Texture
→ Bundle / Loader Cache
→ Native / GPU Resource销毁上层对象只说明某条引用断开了,底层资源是否还能释放,要看是否仍有其他引用和缓存。
三、场景切换后的排查顺序
text
1. 旧 Scene 是否真的退出?
2. 旧节点是否仍被全局对象引用?
3. 事件、schedule、Promise 是否持有组件?
4. SpriteFrame / Texture 是否被新旧页面共享?
5. Loader / Bundle 是否仍缓存?
6. Native 或 GPU 是否延迟释放?不要一开始就调用大量 release API,先确认引用关系。
四、引用计数和共享资源
text
页面 A 使用 Atlas Texture
页面 B 也使用同一 Atlas Texture释放 A 的 SpriteFrame 不能直接释放共享 Texture。资源管理需要知道:
- 谁在使用。
- 谁负责加载。
- 谁负责释放。
- 是否属于全局缓存。
五、内存不下降的几种情况
text
真正泄漏:旧对象仍被引用
缓存保留:系统有意保留资源
分配器保留:已可复用但未归还系统
GPU 延迟:纹理等待渲染队列完成
统计口径:工具显示的是进程保留量它们的解决方法不同。
六、案例:场景 A 切到场景 B
记录三组数据:
text
进入 A 前内存
A 稳定运行内存
切到 B 后稳定内存再重复 A→B→A:
text
每次都增长 → 重点查泄漏或缓存无限增长
第一次高、之后稳定 → 可能是缓存或预热
切换时瞬间高、稳定后下降 → 峰值而非泄漏七、释放设计的工程原则
- 页面管理器负责页面级资源边界。
- 全局资源明确生命周期,不由普通页面随意释放。
- 资源加载和释放成对记录。
- 共享资源使用统一的所有权或引用策略。
- 释放后不再访问旧对象。
- 用重复切换和长时间运行验证趋势。
八、常见误区
误区一:destroy 节点等于释放所有贴图
共享资源、缓存和外部引用仍可能存在。
误区二:调用 release 后进程内存必须立刻归零
分配器和 GPU 释放可能延迟,统计口径也可能不同。
误区三:看到内存不降就不断手动释放
错误释放可能破坏仍在使用的共享资源。
误区四:场景切换测试一次就够
泄漏通常要通过重复切换和长时间运行观察趋势。
九、练习与答案
- 节点销毁后为什么 Texture 可能仍在?
- 如何区分缓存保留和真正泄漏?
- 场景 A→B→A 重复后内存持续增长,优先查什么?
- 为什么不能看到内存不降就随意 release?
答案:
- 共享引用、缓存、Bundle 或 GPU 资源仍持有它。
- 看引用链、缓存策略和重复切换趋势。
- 事件/异步引用、全局管理器、资源缓存和对象池增长。
- 资源可能仍被其他页面使用,错误释放会产生资源失效。
用稳定平台而不是单点判断
测试序列:
text
启动稳定
→ A 稳定
→ B 稳定
→ A 稳定
→ 重复 10 轮记录:
- JS Heap;
- Native/进程内存;
- Texture/GPU;
- Node/Component 数;
- 动态 Asset 缓存数;
- 监听器/定时器数;
- 每轮稳定值和峰值。
如果首轮增长后稳定,常是缓存/预热;每轮稳定值阶梯增长才更可疑。
Creator 2.4.x 实验:故意保留一份引用
ReleaseCycleProbe.ts
ts
const { ccclass, property } = cc._decorator;
const leakedFrames: cc.SpriteFrame[] = [];
@ccclass
export default class ReleaseCycleProbe extends cc.Component {
@property(cc.Sprite)
target: cc.Sprite = null;
private owned: cc.SpriteFrame = null;
open(leak: boolean) {
cc.resources.load(
'perf-lab/scene-image',
cc.SpriteFrame,
(err, frame: cc.SpriteFrame) => {
if (err) {
return;
}
frame.addRef();
this.owned = frame;
this.target.spriteFrame = frame;
if (leak) {
leakedFrames.push(frame);
}
}
);
}
close() {
this.target.spriteFrame = null;
if (this.owned) {
this.owned.decRef();
this.owned = null;
}
}
}leakedFrames 故意模拟全局缓存忘记淘汰。实验后必须清理测试代码,且不能对已多次 addRef 的资源漏掉对应所有权。
对照
- leak=false 反复开关十轮。
- leak=true 反复十轮。
- 记录数组长度与纹理指标。
- 清空全局数组只能移除 JS 引用;若额外 addRef 所有权未正确归还,仍需按协议处理。
场景资源与共享资源
text
Scene A 独占大背景 → A 结束可归还
Common UI Atlas → A/B 共用,不能由 A 擅自释放
GameRoot 音乐 → 应用级
Battle Pool → Battle 级,结束时清池先列所有者,再写释放 API。无法回答“谁 addRef”的资源,不应靠尝试多次 decRef 修复。
真实项目故障:每次切场景多一套网络头像
场景控制器退出时节点销毁,但全局头像缓存以带时间戳 URL 为键;每次进入产生新键并 addRef。稳定值每轮上升。
修复:
- URL 使用内容版本而非随机参数;
- 缓存有字节预算/LRU;
- 场景 Item 清除 Frame;
- 缓存淘汰时归还其所有权;
- 记录 key、尺寸、owner 和最后访问;
- 十轮场景循环验证平台。
诊断与练习答案
- 内存不回启动值是否必然泄漏? 不是,缓存、JIT、分配器和新场景基线都可能提高平台。
- 为什么需要多维指标? JS、Native、Texture/GPU 是不同内存域,单一进程数无法定位。
- Scene 自动结束是否会释放 addRef Asset? 不应假设。显式所有者必须在结束时对应 decRef。
- 如何证明是共享资源而非泄漏? 列出仍活跃使用者和依赖,关闭最后使用者后再观察所有权与多轮平台。
十、本课总结
text
销毁对象、解除引用、释放资源和归还进程内存要分开验证。
内存问题必须结合引用链、缓存策略、峰值和长期趋势分析。十一、下一课预告
text
Lesson068|Label、Animation、Particle 的常见性能问题