Skip to content

Stage03|Runtime Objects & Assets ​

Lesson032|资源依赖图、缓存与引用关系 ​

Tags: #Creator2.x #Stage03 #AssetDependency #Cache #ReferenceDifficulty: ⭐⭐⭐⭐☆


真实问题:两个页面共享图集,关闭一个后另一个黑了 ​

背包页和商城页都使用 ui-common.plist 中的按钮与边框。开发者关闭背包页时“释放它用过的所有 SpriteFrame”,随后商城页的部分图片变黑。

错误不在 Sprite,而在对资源关系的理解仍是线性的:

text
错误想象:Page → Image

真实情况:
Backpack ─┐
          ├→ SpriteFrame → Atlas Texture
Shop ─────┘

一个资源可以依赖其他资源,一个依赖也可以被多个所有者共享。释放必须回答“还有谁需要”,不能只回答“谁先关了页面”。

一、本课目标 ​

资源不是一条孤立的路径,而是一张依赖图。本课回答:

text
SpriteFrame 为什么依赖 Texture?
为什么释放一个资源可能还不能释放底层纹理?
缓存命中和资源所有权是什么关系?

二、资源依赖图 ​

text
Scene
├── Prefab
│   ├── SpriteFrame
│   │   └── Texture
│   └── Material
└── Label
    └── Font

它不是简单的树,因为多个对象可能共享同一个资源:

text
IconA ─┐
IconB ─┼→ SharedTexture
IconC ─┘

释放 IconA 不代表 SharedTexture 可以马上释放。


三、缓存解决什么问题 ​

缓存可以避免:

  • 重复读取同一资源。
  • 重复解码。
  • 重复创建相同运行时对象。
  • 页面反复打开时产生抖动。

但缓存也会带来:

  • 内存长期占用。
  • 资源释放时机变复杂。
  • “看似没人使用但仍在缓存”的误判。

缓存不是垃圾桶,也不是永久保留列表。


四、使用者引用和缓存引用 ​

可以先区分三种关系:

text
静态依赖:Scene / Prefab 序列化数据中记录的资源依赖
动态业务所有权:运行时 load 后由业务通过 addRef / decRef 维护
Asset Manager 缓存:为了复用保存已加载 Asset,但不等同于一次业务引用计数

Creator 2.4.x 能分析并记录静态依赖;但运行时动态加载后再赋给组件的关系,不应假设引擎会自动替业务记录。需要长期持有时调用 asset.addRef(),不再持有时调用 asset.decRef()。

只有业务所有者不再使用资源,并且依赖计数与缓存策略允许释放时,底层对象才可能真正回收。

不同 Creator 2.x API 的缓存和释放语义可能不同,不能凭“调用了 release”就断言 GPU 内存立刻下降。

官方版本依据:Creator 2.4 动态加载与资源释放。


五、共享资源的释放风险 ​

text
释放 Texture
    ↓
SpriteFrame 仍被 UI 使用
    ↓
画面可能失效或出现异常

正确的资源释放流程应该从使用关系出发:

text
确认页面和实例不再使用
    ↓
解除组件和业务引用
    ↓
释放外层资源
    ↓
按依赖和缓存规则处理底层资源

不要只根据某个局部变量是否为 null 判断资源是否安全释放。


六、依赖图和内存峰值 ​

加载一个看起来很小的 Prefab,可能带来:

text
Prefab
→ 多张 SpriteFrame
→ 多个 Texture
→ 材质和字体
→ Spine / Particle 资源

因此分析加载峰值时要记录完整依赖图,而不是只看 Prefab 文件大小。


七、案例:两个页面共享图集 ​

text
ShopPage → ShopAtlas
BagPage  → ShopAtlas

离开 ShopPage 时:

text
ShopPage 不再使用 ShopAtlas
BagPage  仍然使用 ShopAtlas

此时不能因为 ShopPage 关闭就释放图集。资源生命周期要由所有使用者共同决定。


八、常见误区 ​

误区一:一个变量置空就代表资源没人用了 ​

其他组件、缓存和依赖仍可能持有引用。

误区二:释放 SpriteFrame 一定释放 Texture ​

Texture 可能被多个 SpriteFrame 或页面共享。

误区三:缓存越多加载越快,所以不需要释放 ​

缓存会推高内存,尤其是纹理和音频。

误区四:资源依赖是一对一关系 ​

多个上层资源可能共享同一个底层资源。


九、练习与答案 ​

练习 ​

  1. 为什么资源关系更像图而不是树?
  2. 业务引用和缓存引用有什么不同?
  3. 两个页面共享 Texture 时,关闭其中一个页面能否释放 Texture?
  4. 为什么分析内存峰值要展开依赖?

参考答案 ​

  1. 多个资源可以共享同一个底层对象。
  2. 静态依赖由资源系统分析;动态业务所有权需要显式维护;缓存表示资源系统保留 Asset 以便复用,但不等同于业务所有权。
  3. 不能,必须确认另一个页面和其他使用者也不再需要。
  4. 上层 Prefab 很小,但依赖的纹理、字体和材质可能很大。

把资源关系画成两张图 ​

依赖图 ​

描述 Asset 之间“使用谁”:

text
ShopPanel.prefab
├── CommonButton SpriteFrame
├── PriceIcon SpriteFrame
└── Font

CommonButton SpriteFrame
└── ui-common Texture2D

所有权图 ​

描述业务模块“承诺持有谁”:

text
ShopPageOwner
└── ShopPanel.prefab

UICommonCache
└── ui-common 共享图集

依赖图由资源系统解析,所有权图需要项目设计。两张图不能混为“有一个变量引用所以安全”。

Creator 2.4.x 编辑器实验:观察共享 Asset 身份 ​

使用 Creator 编辑器导入和绑定资源,不手工修改 .meta、.prefab、.scene 或缓存目录。

准备 ​

  1. 在 assets/resources/dependency-lab/ 导入一张图片。
  2. 创建两个 Sprite 节点 A、B。
  3. 创建脚本并绑定两个 Sprite。
ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class DependencyProbe extends cc.Component {
    @property(cc.Sprite)
    spriteA: cc.Sprite = null;

    @property(cc.Sprite)
    spriteB: cc.Sprite = null;

    private ownedFrame: cc.SpriteFrame = null;

    loadShared() {
        cc.resources.load(
            'dependency-lab/shared-icon',
            cc.SpriteFrame,
            (err, frame: cc.SpriteFrame) => {
                if (err) {
                    cc.error(err);
                    return;
                }

                this.ownedFrame = frame;
                frame.addRef();
                this.spriteA.spriteFrame = frame;
                this.spriteB.spriteFrame = frame;

                cc.log(
                    'same asset=',
                    this.spriteA.spriteFrame
                        === this.spriteB.spriteFrame
                );
            }
        );
    }

    clearA() {
        this.spriteA.spriteFrame = null;
        cc.log(
            'B still references frame=',
            this.spriteB.spriteFrame === this.ownedFrame
        );
    }

    releaseOwner() {
        this.spriteA.spriteFrame = null;
        this.spriteB.spriteFrame = null;

        if (this.ownedFrame) {
            this.ownedFrame.decRef();
            this.ownedFrame = null;
        }
    }

    onDestroy() {
        this.releaseOwner();
    }
}

实验 A:同一路径的共享身份 ​

加载后确认 A、B 引用同一个 SpriteFrame。把 A 清空,B 仍持有并显示同一资源。

预期结论:

text
清空一个组件字段
≠ 所有使用者都结束
≠ 底层 Texture 立即释放

实验 B:显式所有权 ​

addRef 表示实验组件声明一份动态资源所有权;decRef 归还这份所有权。必须成对,且不能让 A、B 各自随意对共享 Asset 重复归还。

实验 C:观察依赖而不是猜依赖 ​

在 Creator 资源管理器中检查图片导入后的 Texture/SpriteFrame 子资源,再使用编辑器或项目调试工具查看引用。不要手改序列化文件寻找 UUID。

引用计数不等于 JavaScript 变量数 ​

下面三件事不同:

text
JavaScript 变量指向 Asset
组件序列化/运行时字段引用 Asset
Asset Manager 的引用计数与缓存记录

因此:

ts
this.frame = null;

只说明一个 JS 字段不再指向 Asset;它不会自动证明:

  • Sprite 组件没有引用;
  • 其他页面没有引用;
  • Prefab 实例没有引用;
  • Asset 缓存已经移除;
  • 依赖 Texture 可以释放;
  • GPU 内存立即归还系统。

引用计数是资源管理协议,不是 JS 可达性计数器。

共享资源的安全所有者 ​

对于 ui-common 这类共享资源,可以明确由模块缓存拥有:

text
UICommonCache.open()
→ 首个使用者加载并 addRef
→ users = 1

第二页面打开
→ users = 2

页面关闭
→ users -= 1

users = 0
→ 清除组件引用
→ decRef
→ 允许 Asset Manager 在合适时机释放

业务计数与引擎引用计数可以不是同一个数字,但责任必须集中。不要让每个 Sprite 自己决定共享图集生死。

真实项目故障:皮肤切换造成内存阶梯增长 ​

现象 ​

每切换一次角色皮肤,纹理内存上升一截,切回旧皮肤也不下降。

根因链 ​

text
每次 load 新皮肤
→ Cache Map 永久保存 SpriteFrame
→ 旧 Sprite 已换图,但 Cache 仍持有
→ 淘汰策略从未执行
→ 依赖 Texture 长期可达

修复 ​

为缓存设计可观察的预算:

  • 记录 path、Asset 类型、估算内存、最后访问时间;
  • 区分常驻、场景级和临时资源;
  • 淘汰前先清除业务组件引用;
  • 只归还缓存真正拥有的引用;
  • 通过目标平台内存快照验证,而不是期待数值瞬间归零。

实验与故障排查 ​

decRef 后资源仍在 ​

可能还有其他显式引用、组件引用、缓存记录或依赖使用者;也可能底层内存回收不是同步发生。输出所有权日志,检查谁 addRef、谁 decRef,而不是继续多调一次 decRef。

释放后出现黑图 ​

这通常说明仍有渲染组件使用该资源,或共享依赖被错误归还。先定位黑图 SpriteFrame、其 Texture 和释放调用栈,再恢复正确所有权。

引用计数变成异常值 ​

检查重复 release、onDisable 与 onDestroy 双重归还、加载失败仍归还、对象池复用未重新获取等路径。释放函数应幂等,并清空已归还的 owned 字段。

版本边界与官方入口 ​

本课以 Creator 2.4.x Asset Manager 的 Asset.addRef/decRef 为主。旧 cc.loader 项目有不同的自动释放与缓存习惯,不能直接混用释放策略;Creator 3.x 的 API 组织也不同。

深化练习与答案 ​

  1. A、B 两个页面共享一个图集,谁应该调用 decRef? 应由当初 addRef 并声明所有权的模块调用。若由共享缓存统一持有,页面只登记使用者,最后一个使用者退出后由缓存归还。
  2. sprite.spriteFrame = null 后为什么纹理仍可能存在? 其他 SpriteFrame、Sprite、缓存或显式引用仍可能依赖同一 Texture,且底层释放不保证同步反映到系统内存。
  3. 为什么“看到 refCount 为 1 就强制释放”危险? 一个数字无法解释这份引用属于谁,也无法证明组件或依赖图状态;绕过所有权协议会让仍在使用的对象失效。
  4. 缓存如何证明没有泄漏? 给出容量/内存预算、命中淘汰日志、场景循环后的稳定平台、资源所有者清单和目标设备快照,而不是只展示一次关闭页面后的数值。

十、本课总结 ​

text
资源加载和释放要看依赖图。
缓存降低重复加载,但会增加内存和管理复杂度。
共享资源不能按单个页面的生命周期直接释放。
Creator 2.4.x 必须区分静态依赖、动态 addRef / decRef 和 Asset Manager 缓存。
资源真正回收需要业务所有权、缓存策略和依赖关系共同满足。

十一、下一课预告 ​

text
Lesson033|Texture、SpriteFrame、Atlas 为什么不是同一个对象?