外观
Stage03|Runtime Objects & Assets
Lesson036|阶段复习:从磁盘资源到运行时对象,再到安全释放
Tags: #Creator2.x #Stage03 #Scene #Prefab #Asset #MemoryDifficulty: ⭐⭐⭐⭐☆
阶段挑战:你能解释一次活动页的完整生命吗
最终项目中,活动页不是“加载 Prefab 然后显示”这么简单。它经历:
text
源图片和 Prefab 被 AssetDB 导入
→ UUID 与序列化引用建立
→ 构建系统把活动 Bundle 发布
→ 运行时加载 Bundle 和 Prefab
→ instantiate 还原 Node/Component
→ 组件建立事件与资源引用
→ Renderer 使用 SpriteFrame/Texture
→ 页面关闭并清理监听
→ 节点销毁
→ 资源所有者归还引用任何一步含糊,都会在另一端表现为丢引用、竞态、重复回调、黑图或内存增长。
本课不是把 L022~L035 再列一遍,而是要求你用一个可运行项目把整条链串起来,并能根据证据定位故障层。
一、本课目标
本课串联 Lesson022~Lesson035,完成 Runtime Objects & Assets 阶段闭环。
核心问题是:
一个编辑器资源,如何变成运行时对象,被场景使用,最后安全退出?
二、阶段完整数据流
text
源文件
↓
AssetDB 导入
↓
.meta / UUID / 子资源
↓
构建 / Bundle / 远程分发
↓
Loader 按路径加载
↓
依赖图和缓存
↓
Scene / Prefab 反序列化
↓
Node / Component 实例
↓
生命周期、事件、渲染和业务使用
↓
解除引用、销毁对象、释放资源这条链路中,每一层都有自己的身份、缓存和生命周期。
三、Scene、Prefab、Node 的关系
text
Scene / Prefab:序列化描述
Node:运行时层级和空间对象
Component:挂载能力
Asset:被组件引用的资源实例化时:
text
Prefab 描述
→ 新 Node 树
→ 新 Component 实例
→ 共享或恢复 Asset 引用运行时修改实例状态不会自动修改 Prefab 默认值。
四、UUID、.meta 和 AssetDB
text
资源文件
↔ .meta / UUID
↔ AssetDB 索引
↔ Scene / Prefab 引用稳定原则:
- 替换已有资源内容时保留原
.meta。 - 新资源让 Creator 生成新的
.meta。 - 不手工伪造 UUID。
- 移动和重命名资源后等待编辑器导入完成。
五、动态加载的安全模型
ts
load(path, callback)必须同时考虑:
text
路径和类型
↓
缓存和依赖
↓
异步返回顺序
↓
页面是否仍有效
↓
加载失败和回退一段正确的资源代码不只是“能拿到资源”,还要能处理过期请求和失败状态。
六、事件和场景生命周期
节点事件依赖节点树,资源异步回调依赖请求和对象生命周期:
text
页面打开
→ 注册事件
→ 发起加载
→ 显示资源
→ 页面关闭
→ 取消事件
→ 忽略过期回调
→ 解除资源引用如果只清理显示节点,不清理事件和异步关系,场景仍可能被旧对象或闭包持有。
七、阶段综合案例:活动页面
资源结构
text
activity.prefab
├── ActivityPanel
├── Banner SpriteFrame
├── Reward Icon SpriteFrame
└── ActivityController进入页面
text
加载 Bundle / Prefab
→ 加载 Texture、SpriteFrame、配置
→ instantiate
→ 设置 parent
→ 绑定组件引用
→ onLoad / onEnable
→ 显示页面页面关闭
text
active=false 或 destroy
→ 取消事件和定时器
→ 使异步请求失效
→ 解除页面引用
→ 判断资源是否仍被其他页面使用
→ 按缓存策略释放八、阶段练习
题目一
为什么一个 20 KB 的 Prefab 可能加载出很大的内存峰值?
题目二
移动一张已被多个 Prefab 引用的图片时,为什么不能直接删除 .meta?
题目三
页面 A 请求图片后切到页面 B,A 的回调晚于 B 返回,应该如何处理?
题目四
销毁页面节点后,为什么对应 Texture 仍可能留在内存?
题目五
对象池为什么会降低创建成本,同时增加资源管理难度?
九、阶段练习参考答案
题目一
Prefab 可能依赖大纹理、字体、音频、材质和其他资源,文件大小不能代表完整依赖图。
题目二
.meta 中包含 UUID 和导入设置,删除后可能生成新身份,使已有 Scene / Prefab 引用失效。
题目三
使用请求 ID、页面生命周期标记或取消句柄,确保只有当前有效请求能够应用结果。
题目四
其他页面、Loader 缓存、Bundle、异步回调或对象池仍可能持有资源引用。
题目五
池化复用 Node 和 Component,减少分配和生命周期成本,但池子会保留对象、组件和资源引用。
十、Stage03 面试题
1. Scene 文件和运行时 Scene 有什么区别?
前者是序列化数据,后者是引擎创建并管理的运行时对象树。
2. Prefab 实例之间共享什么?
Node、Component 和业务状态通常独立,Texture、SpriteFrame 等资源可能共享。
3. AssetDB 和 Loader 的区别是什么?
AssetDB 主要负责编辑器导入、UUID 和索引,Loader 负责运行时加载、缓存和依赖处理。
4. 为什么 destroy 不等于 release asset?
destroy 主要结束 Node / Component 生命周期,资源还可能被其他对象或缓存使用。
5. 为什么动态加载需要生命周期校验?
异步结果可能晚于页面或对象的有效期,必须防止旧回调写入无效对象。
6. 为什么 Atlas 既能优化也能浪费?
它可以减少纹理切换和帮助合批,但加载一个小图可能带来整张大纹理的内存成本。
阶段综合实验:SeasonPanel 生命周期实验室
资源和序列化对象全部通过 Creator 2.4.x 编辑器创建、绑定和移动。不要手工修改
.meta、.scene、.prefab、.plist或生成目录。
一、资源结构
通过编辑器建立:
text
assets/
├── resources/
│ └── stage03-lab/
│ ├── SeasonPanel.prefab
│ ├── season-bg.png
│ └── reward-icon.png
└── scripts/
├── SeasonPanel.ts
└── Stage03Lab.tsPrefab 结构:
text
SeasonPanel
├── Background/Sprite
├── Title/Label
├── RewardIcon/Sprite
├── ClaimButton/Button
└── CloseButton/Button在 Inspector 中绑定 Prefab 内部 Label、Sprite 和按钮节点引用。不要在脚本中硬编码 UUID。
二、Panel 组件
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class SeasonPanel extends cc.Component {
@property(cc.Label)
titleLabel: cc.Label = null;
@property(cc.Sprite)
rewardIcon: cc.Sprite = null;
private bindVersion = 0;
private seasonId = 0;
configure(seasonId: number) {
this.seasonId = seasonId;
const version = ++this.bindVersion;
this.titleLabel.string = 'Season ' + seasonId;
cc.resources.load(
'stage03-lab/reward-icon',
cc.SpriteFrame,
(err, frame: cc.SpriteFrame) => {
if (err) {
cc.error('[SeasonPanel] icon failed', err);
return;
}
if (version !== this.bindVersion
|| !cc.isValid(this.node)) {
return;
}
this.rewardIcon.spriteFrame = frame;
}
);
}
onEnable() {
this.node.on(
'season-refresh',
this.onSeasonRefresh,
this
);
}
onDisable() {
this.node.off(
'season-refresh',
this.onSeasonRefresh,
this
);
this.bindVersion++;
}
private onSeasonRefresh() {
cc.log('[SeasonPanel] refresh', this.seasonId);
}
}这里故意使用 Node 自定义事件做局部实验。真实全局服务应使用项目统一 EventBus,并保持相同的订阅/取消原则。
三、入口与资源所有者
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class Stage03Lab extends cc.Component {
@property(cc.Node)
container: cc.Node = null;
private panelPrefab: cc.Prefab = null;
private panel: cc.Node = null;
private openVersion = 0;
openSeason(seasonId: number) {
const version = ++this.openVersion;
cc.resources.load(
'stage03-lab/SeasonPanel',
cc.Prefab,
(err, prefab: cc.Prefab) => {
if (err) {
cc.error('[Stage03Lab] prefab failed', err);
return;
}
if (version !== this.openVersion
|| !cc.isValid(this.node)) {
return;
}
if (!this.panelPrefab) {
this.panelPrefab = prefab;
this.panelPrefab.addRef();
}
this.closePanelOnly();
this.panel = cc.instantiate(prefab);
this.container.addChild(this.panel);
const view = this.panel.getComponent(SeasonPanel);
view.configure(seasonId);
cc.log('[Stage03Lab] opened', seasonId);
}
);
}
closePanelOnly() {
this.openVersion++;
if (this.panel && cc.isValid(this.panel)) {
this.panel.destroy();
}
this.panel = null;
}
releaseLab() {
this.closePanelOnly();
if (this.panelPrefab) {
this.panelPrefab.decRef();
this.panelPrefab = null;
}
}
onDestroy() {
this.releaseLab();
}
}注意:openSeason 内调用 closePanelOnly 会再次增加 openVersion,导致当前 version 失效的风险。这个代码故意留下一个可诊断缺陷。
正确做法是拆分“关闭旧实例”和“取消当前打开请求”:
ts
private destroyCurrentPanel() {
if (this.panel && cc.isValid(this.panel)) {
this.panel.destroy();
}
this.panel = null;
}
closePanel() {
this.openVersion++;
this.destroyCurrentPanel();
}在 load 回调中调用 destroyCurrentPanel,不能调用会递增请求版本的公开 closePanel。这体现了状态机方法必须区分“内部状态转换”和“用户取消命令”。
四、实验步骤
- 打开 Season 1,观察 Prefab 加载、instantiate 和内部图标回调。
- 关闭页面,确认 onDisable 执行且异步代次失效。
- 快速连续打开 Season 2、Season 3,最终只允许 Season 3 提交。
- 重复开关五次,确认一次 refresh 只打印一次。
- 只 destroy Panel,确认 Lab 仍持有 Prefab。
- 调用 releaseLab,确认所有者归还 Prefab 引用。
- 在 Creator 中重命名 reward-icon,通过实验观察路径字符串失效;再恢复代码路径。
- 使用错误类型加载 Prefab,记录 err 分支。
五、每步应该看到什么
text
静态内部引用:instantiate 后立即恢复
动态图标:load 回调后赋值
快速切换:旧 season 结果被 generation 拒绝
关闭页面:节点生命周期结束,Prefab Asset 所有权仍独立存在
releaseLab:只归还 Lab 自己 addRef 的引用
资源重命名:UUID 静态引用与动态路径表现不同Stage03 故障定位矩阵
| 现象 | 优先层级 | 第一批证据 |
|---|---|---|
| Inspector 字段为 null | 序列化/脚本字段 | 组件类型、字段名、编辑器绑定、脚本编译 |
| resources.load 找不到 | 运行时路径 | resources 根、路径、扩展名、大小写、类型 |
| Prefab 创建后缺组件 | instantiate/资源版本 | Prefab 实际内容、脚本注册、错误日志 |
| 点击一次执行多次 | 事件生命周期 | on/off 次数、target/currentTarget、Button ClickEvents |
| 快速切换显示旧图 | 异步竞态 | 请求 ID、业务键、回调完成顺序 |
| 关闭页面后黑图 | 资源所有权 | addRef/decRef 调用栈、共享依赖、仍活跃 Sprite |
| 节点消失但内存不降 | 多层生命周期 | Node 数、Asset 缓存、Texture、对象池、循环平台 |
| 构建包找不到远程资源 | Bundle/发布 | Bundle 版本、最终 URL、状态码、CDN/本地缓存 |
矩阵的作用是确定第一组证据,不是看到某现象就直接下结论。
阶段真实案例:节日活动热更后偶发黑屏
现象组合
- 新 Bundle 能下载;
- Panel Prefab 可以 instantiate;
- 一部分按钮黑图;
- 返回大厅再进入,有时恢复;
- 内存中同时存在新旧两张 Atlas。
分层推理
text
Prefab 能创建
→ Bundle 入口和脚本大体可用
部分图片失败
→ 查 SpriteFrame → Texture 依赖与远程 URL
新旧 Atlas 同时存在
→ 查版本缓存键与旧所有者
重进偶尔恢复
→ 查竞态和缓存命中顺序最终发现发布入口先切到新配置,新 Atlas 尚未完成所有 CDN 节点同步;失败回退又从旧缓存取 Frame,而页面缓存同时 addRef 新旧资源。
系统修复
- Bundle 使用不可变版本目录;
- 全部上传验证后再切入口;
- 页面请求带 Bundle 版本和 generation;
- 失败回退只使用同一兼容版本;
- 缓存所有权按版本隔离并设置淘汰;
- 连续开关与版本切换做自动化回归。
这类事故无法靠“多 load 一次”解决,因为它跨越发布一致性、竞态和资源所有权三层。
阶段能力验收
完成 Stage03 不以读完文档为准。你应该能够独立完成:
- 在编辑器中创建带内部引用的 Prefab,并解释序列化如何恢复。
- 用 Inspector 绑定固定资源,用 resources 加载动态资源,并说明选择依据。
- 写出带错误、代次和对象有效性检查的异步代码。
- 画出 Prefab、SpriteFrame、Texture 的依赖图。
- 为一个动态资源缓存定义 addRef/decRef 所有者。
- 构造一次重复监听并用日志证明根因。
- 构造一次旧请求覆盖新请求并修复。
- 解释 destroy、对象池、Asset 释放和 GPU 内存的差别。
- 不修改
.meta和生成目录完成资源迁移与验证。
新增综合练习与答案
练习一
商城 Prefab 静态引用公共按钮图集,商品图通过 resources 动态加载。关闭商城时哪些资源能由商城释放?
答案: 商城只能归还自己显式拥有的动态资源/缓存引用。公共按钮图集若由更长生命周期 UI 模块或静态依赖拥有,商城不能擅自释放。商品图也要先确认没有列表池和其他页面共享,再由实际 addRef 所有者归还。
练习二
为什么 cc.isValid(this.node) 不能解决搜索结果竞态?
答案: 页面可能仍有效,但搜索关键词已从 A 变成 B。对象有效性只能证明接收者活着,请求 ID 或业务键才能证明结果仍属于当前状态。
练习三
Prefab 中固定 Label 引用重命名字段后变成 null,为什么重新 load Prefab 没用?
答案: 问题在序列化字段契约和编辑器绑定,不在运行时缓存。应确认字段迁移策略,并通过 Creator 编辑器重新绑定、保存和验证所有受影响资源,不能手改 Prefab 文本。
练习四
一个 SpriteFrame 已 decRef,为什么不能断言 Texture 会立刻销毁?
答案: 其他 Frame/Asset/组件可能仍依赖 Texture,Asset Manager 还要处理依赖关系,GPU 和分配器释放也可能延迟。
练习五
远程 Bundle 的 config 返回 200,能否证明活动可用?
答案: 不能。还需验证版本匹配、内部 Asset 路径、依赖文件、hash/大小、解码、instantiate 和业务初始化。配置成功只是链路第一段。
版本边界与验证入口
Stage03 主线固定为 Creator 2.4.x Asset Manager。旧 cc.loader 只用于存量项目理解,不与新资源引用计数混为一套;Creator 3.x 的模块 API 与序列化结构需单独迁移。
十一、Stage03 总结
text
编辑器资源不是运行时对象。
AssetDB 管导入和身份,Loader 管运行时加载。
Scene / Prefab 反序列化后才成为 Node / Component。
资源通过依赖图和缓存被多个对象共享。
场景退出要同时处理对象、事件、异步请求和资源引用。
destroy、对象池和资源释放是不同层级的策略。最终能力是:
能从一个“页面加载慢、切换后内存不降或资源显示异常”的问题,沿资源身份、依赖、对象创建、生命周期和释放链路逐层排查。
十二、下一阶段预告
Stage03 完成,下一阶段进入:
text
Stage04|Renderer
Lesson037|Cocos 2.x 渲染流程总览下一阶段将继续追踪:
text
Node / Component 状态
→ RenderData / Assembler
→ Material / Texture / Render State
→ Batch / DrawCall
→ GPU / Screen