外观
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 或页面共享。
误区三:缓存越多加载越快,所以不需要释放
缓存会推高内存,尤其是纹理和音频。
误区四:资源依赖是一对一关系
多个上层资源可能共享同一个底层资源。
九、练习与答案
练习
- 为什么资源关系更像图而不是树?
- 业务引用和缓存引用有什么不同?
- 两个页面共享 Texture 时,关闭其中一个页面能否释放 Texture?
- 为什么分析内存峰值要展开依赖?
参考答案
- 多个资源可以共享同一个底层对象。
- 静态依赖由资源系统分析;动态业务所有权需要显式维护;缓存表示资源系统保留 Asset 以便复用,但不等同于业务所有权。
- 不能,必须确认另一个页面和其他使用者也不再需要。
- 上层 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或缓存目录。
准备
- 在
assets/resources/dependency-lab/导入一张图片。 - 创建两个 Sprite 节点 A、B。
- 创建脚本并绑定两个 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 组织也不同。
深化练习与答案
- A、B 两个页面共享一个图集,谁应该调用 decRef? 应由当初 addRef 并声明所有权的模块调用。若由共享缓存统一持有,页面只登记使用者,最后一个使用者退出后由缓存归还。
sprite.spriteFrame = null后为什么纹理仍可能存在? 其他 SpriteFrame、Sprite、缓存或显式引用仍可能依赖同一 Texture,且底层释放不保证同步反映到系统内存。- 为什么“看到 refCount 为 1 就强制释放”危险? 一个数字无法解释这份引用属于谁,也无法证明组件或依赖图状态;绕过所有权协议会让仍在使用的对象失效。
- 缓存如何证明没有泄漏? 给出容量/内存预算、命中淘汰日志、场景循环后的稳定平台、资源所有者清单和目标设备快照,而不是只展示一次关闭页面后的数值。
十、本课总结
text
资源加载和释放要看依赖图。
缓存降低重复加载,但会增加内存和管理复杂度。
共享资源不能按单个页面的生命周期直接释放。
Creator 2.4.x 必须区分静态依赖、动态 addRef / decRef 和 Asset Manager 缓存。
资源真正回收需要业务所有权、缓存策略和依赖关系共同满足。十一、下一课预告
text
Lesson033|Texture、SpriteFrame、Atlas 为什么不是同一个对象?