Skip to content

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 释放可能延迟,统计口径也可能不同。

误区三:看到内存不降就不断手动释放 ​

错误释放可能破坏仍在使用的共享资源。

误区四:场景切换测试一次就够 ​

泄漏通常要通过重复切换和长时间运行观察趋势。


九、练习与答案 ​

  1. 节点销毁后为什么 Texture 可能仍在?
  2. 如何区分缓存保留和真正泄漏?
  3. 场景 A→B→A 重复后内存持续增长,优先查什么?
  4. 为什么不能看到内存不降就随意 release?

答案:

  1. 共享引用、缓存、Bundle 或 GPU 资源仍持有它。
  2. 看引用链、缓存策略和重复切换趋势。
  3. 事件/异步引用、全局管理器、资源缓存和对象池增长。
  4. 资源可能仍被其他页面使用,错误释放会产生资源失效。

用稳定平台而不是单点判断 ​

测试序列:

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 的资源漏掉对应所有权。

对照 ​

  1. leak=false 反复开关十轮。
  2. leak=true 反复十轮。
  3. 记录数组长度与纹理指标。
  4. 清空全局数组只能移除 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 和最后访问;
  • 十轮场景循环验证平台。

诊断与练习答案 ​

  1. 内存不回启动值是否必然泄漏? 不是,缓存、JIT、分配器和新场景基线都可能提高平台。
  2. 为什么需要多维指标? JS、Native、Texture/GPU 是不同内存域,单一进程数无法定位。
  3. Scene 自动结束是否会释放 addRef Asset? 不应假设。显式所有者必须在结束时对应 decRef。
  4. 如何证明是共享资源而非泄漏? 列出仍活跃使用者和依赖,关闭最后使用者后再观察所有权与多轮平台。

十、本课总结 ​

text
销毁对象、解除引用、释放资源和归还进程内存要分开验证。
内存问题必须结合引用链、缓存策略、峰值和长期趋势分析。

十一、下一课预告 ​

text
Lesson068|Label、Animation、Particle 的常见性能问题