外观
Stage03|Runtime Objects & Assets
Lesson030|resources.load / loadRes 背后的加载流程
Tags: #Creator2.x #Stage03 #Resources #Loader #AsyncDifficulty: ⭐⭐⭐⭐☆
真实问题:为什么换装面板第一次打开是空白
玩家点击“换装”,代码立即创建 30 个 Item,并为每个 Item 调用一次 cc.resources.load。开发机第二次打开面板很快,第一次打开却有一段明显空白;快速关闭面板时,控制台还会出现无效节点错误。
问题不是一句“异步加载比较慢”就能解释。一次加载请求至少涉及:
text
业务提交逻辑路径与类型
→ Bundle 解析资源信息
→ 检查缓存或进行中的任务
→ 获取资源本体及依赖
→ 创建运行时 Asset
→ 回调排入 JavaScript 执行
→ 业务确认页面和请求仍有效
→ 组件使用资源第二次快,可能是缓存命中;关闭后报错,是回调比页面寿命更长;30 个请求是否真的读取 30 次,还取决于路径去重、共享依赖和缓存状态。
本课不把 load 当成“读文件函数”,而把它看成一个异步资源任务。
一、本课目标
本课从常用 API 出发:
ts
cc.resources.load('ui/icon', cc.SpriteFrame, callback);理解它背后的路径解析、依赖加载、缓存、回调和错误处理。
二、动态加载不是同步读取文件
调用加载 API 后,通常经历:
text
提交资源路径
↓
解析资源和类型
↓
检查缓存
↓ 未命中
加载依赖和本体
↓
解码 / 创建运行时对象
↓
回调通知因此下面代码不能直接使用异步结果:
ts
let frame: cc.SpriteFrame;
cc.resources.load('ui/icon', cc.SpriteFrame, (err, value) => {
frame = value;
});
this.sprite.spriteFrame = frame; // 可能仍然是 undefined应该在回调或 Promise 封装完成后使用资源。
三、2.x 中的常见加载入口
Creator 2.4.x 主线应优先使用新的 Asset Manager 入口:
ts
cc.resources.load('ui/icon', cc.SpriteFrame, callback);旧项目中还可能看到兼容接口:
ts
cc.loader.loadRes('ui/icon', cc.SpriteFrame, callback);cc.loader.loadRes 属于 2.4 之前的旧资源系统;Creator 2.4 虽然暂时保留兼容,但新代码不应继续把它作为首选,也不能假设它与 Asset Manager 的依赖记录、引用计数和释放语义完全相同。
无论维护哪一种代码,都应遵守:
- 使用正确资源路径。
- 指定或确认资源类型。
- 检查错误。
- 管理重复请求和释放。
课程后续以 Creator 2.4.x 的 cc.resources、cc.assetManager 和 cc.AssetManager.Bundle 为主;旧 cc.loader 只用于解释存量项目迁移,不混入主流程。
官方版本依据:Asset Manager、Asset Manager 升级指南。
四、缓存命中和重复请求
第二次加载同一个资源时,Loader 可能返回缓存对象或复用正在进行的请求:
text
第一次 load
↓ 未命中
开始读取
第二次 load
↓
发现加载中或已缓存
↓
复用结果业务代码不能假设每次 load 都会重新读取磁盘或网络,也不能假设每次回调都会返回一个全新的资源对象。
五、加载依赖
请求 SpriteFrame 时,可能还需要:
text
SpriteFrame
↓ 依赖
Texture / Atlas
↓
底层 GPU 资源Loader 需要先后处理这些依赖,最终才能让 Sprite 使用资源。
如果资源依赖很多,加载时间和内存峰值可能明显增加。性能分析不能只看最外层文件大小。
六、错误处理是加载流程的一部分
ts
cc.resources.load('ui/icon', cc.SpriteFrame, (err, frame) => {
if (err) {
cc.error('load icon failed', err);
this.showFallbackIcon();
return;
}
if (!this.isValid) {
return;
}
this.sprite.spriteFrame = frame;
});至少要考虑:
- 路径错误。
- 类型错误。
- 构建时资源没有被打包。
- 网络或远程资源失败。
- 回调返回时对象已经销毁。
- 同一页面重复发起请求。
七、加载状态管理
页面级资源可以维护状态:
text
idle
→ loading
→ ready
→ failed
→ released这样可以避免:
- 加载按钮重复点击。
- 失败后 UI 永久卡在 loading。
- 旧请求覆盖新页面。
- 释放后仍当作 ready 使用。
八、案例:图片列表按需加载
ts
loadIcon(path: string, done: (frame: cc.SpriteFrame) => void) {
cc.resources.load(path, cc.SpriteFrame, (err, frame) => {
if (err) {
done(this.defaultFrame);
return;
}
done(frame);
});
}真正的列表还需要:
- 可见区域优先级。
- 旧 Item 回收后忽略回调。
- 同路径请求合并。
- 默认图和失败图。
- 资源缓存上限。
九、常见误区
误区一:调用 load 后下一行就能使用结果
动态加载通常是异步流程。
误区二:同一路径每次都会重新读取
Loader 可能命中缓存或复用正在进行的请求。
误区三:回调返回就一定可以更新当前 UI
页面可能已经关闭或请求已经过期。
误区四:只检查 err 不检查资源类型和有效状态
类型、生命周期和请求版本同样重要。
十、练习与答案
练习
- 动态加载的高层步骤是什么?
- 为什么加载 SpriteFrame 可能还要处理 Texture?
- 如何防止旧页面的异步回调更新新页面?
- 重复加载同一资源时,Loader 可能如何处理?
参考答案
- 解析路径、查缓存、加载依赖和本体、解码创建对象、回调通知。
- SpriteFrame 可能依赖 Texture 或 Atlas。
- 使用对象有效性、请求 ID 或页面生命周期状态检查。
- 命中缓存或复用进行中的请求,不一定重新读取。
Creator 2.4.x 编辑器实验:亲眼观察异步、缓存和类型
使用 Creator 编辑器创建资源、节点和组件绑定。不要手工编辑
.scene、.prefab、.meta或生成目录。
实验准备
- 在 Creator 资源管理器中创建
assets/resources/load-lab/。 - 导入一张小图片,命名
icon-a.png,等待 AssetDB 完成。 - 创建
ResourceLoadLab.scene,放置一个 Sprite 和三个 Button。 - 创建
LoadFlowProbe.ts,挂到 Canvas,通过 Inspector 绑定 Sprite。
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class LoadFlowProbe extends cc.Component {
@property(cc.Sprite)
target: cc.Sprite = null;
private request = 0;
loadCorrect() {
const id = ++this.request;
const begin = Date.now();
cc.log('[load] submit', id);
cc.resources.load(
'load-lab/icon-a',
cc.SpriteFrame,
(err, frame: cc.SpriteFrame) => {
cc.log('[load] callback', id, Date.now() - begin + 'ms');
if (err) {
cc.error('[load] failed', err);
return;
}
if (id !== this.request || !cc.isValid(this.node)) {
cc.warn('[load] stale result', id);
return;
}
cc.log(
'[load] type',
frame instanceof cc.SpriteFrame
);
this.target.spriteFrame = frame;
}
);
cc.log('[load] after submit', id);
}
loadWrongPath() {
cc.resources.load(
'load-lab/not-exists',
cc.SpriteFrame,
(err) => cc.log('[load] expected error', !!err)
);
}
invalidate() {
this.request++;
this.target.spriteFrame = null;
}
}实验 A:证明回调晚于当前同步代码
点击绑定 loadCorrect 的按钮。
预期日志顺序:
text
[load] submit 1
[load] after submit 1
[load] callback 1 ...无论缓存是否命中,都不应把业务建立在“调用下一行已经拿到结果”上。
实验 B:观察重复请求
连续点击两次 loadCorrect,比较两次耗时与返回对象:
ts
cc.log('same frame=', this.lastFrame === frame);
this.lastFrame = frame;第二次可能更快,且可能取得缓存中的同一 Asset 实例。不要把“每次 load 都创建一个全新 SpriteFrame”作为业务假设。
实验 C:错误必须进入显式分支
点击 loadWrongPath。预期 err 为真,Sprite 不被赋值,界面也不应永久停留在 loading。真实页面还应显示默认图或重试入口。
实验 D:制造过期结果
点击加载后立即点击 invalidate。如果加载回调稍后返回,预期打印 stale result,而不是更新已失效页面。
实验失败时怎么排查
正确路径仍报找不到
text
资源是否位于 resources 根下
→ 路径是否相对 resources
→ 是否错误携带扩展名
→ 大小写是否一致
→ 请求类型是否匹配导入子资源
→ AssetDB 是否完成导入callback 完全没有日志
先确认脚本编译、按钮绑定和方法调用,再确认回调参数位置是否符合项目实际的 2.4.x 类型声明。不要通过在 update 中重复调用 load 来“确保执行”。
第二次仍然慢
可能是请求路径不同、资源已被释放、依赖来自远端、性能瓶颈在图片解码/GPU 上传或面板创建,而不是 Loader 读取。需要分别测量提交、回调、赋值和首帧可见时间。
真实项目故障:列表 Item 回收后显示了别人的头像
时间线
text
Item#7 绑定玩家 A
→ 请求 avatar/A
→ 列表滚动,Item#7 被回收并绑定玩家 B
→ 请求 avatar/B
→ A 的回调较晚返回
→ Item#7 显示 A,但数据已经是 B错误点不是资源路径,而是把“Item 实例”误当成“请求身份”。
修复应让绑定代次参与校验:
ts
private bindVersion = 0;
bind(data: PlayerData) {
const version = ++this.bindVersion;
this.nameLabel.string = data.name;
cc.resources.load(
data.avatarPath,
cc.SpriteFrame,
(err, frame: cc.SpriteFrame) => {
if (err
|| version !== this.bindVersion
|| !cc.isValid(this.node)) {
return;
}
this.avatar.spriteFrame = frame;
}
);
}
unuse() {
this.bindVersion++;
this.avatar.spriteFrame = this.placeholder;
}这只是“结果归属”修复;资源引用和释放由列表缓存策略统一管理,不能让每个 Item 随意释放共享头像。
深化练习
- 为什么缓存命中也不能允许调用方在
load下一行读取结果? - 一个页面同时请求 A、B、C 三张图,C 先完成是否代表加载系统乱序?
- 如何区分“资源回调完成”和“画面已经渲染出首帧”?
- 路径正确、无 err、Sprite 仍为空,应继续检查哪些对象层次?
推导答案
- API 契约是异步回调,缓存只改变内部耗时,不应改变调用方控制流,否则同一代码在冷缓存和热缓存下会有两套时序。
- 不代表。异步任务按缓存、依赖、大小和 I/O 状态各自完成,提交顺序不是完成顺序;业务如需顺序,应自己建立聚合或状态机。
- 回调说明 Asset 可交给业务;赋给组件后还要经过组件更新、渲染数据生成和下一帧提交。应分别记录 load callback、赋值时刻和下一帧可见/渲染指标。
- 检查返回类型、
targetSprite 是否有效、节点是否 active、SpriteFrame 是否真正赋值、材质/颜色/opacity、节点尺寸和层级,而不是再次 load。
十一、本课总结
text
load API 是异步资源流程的入口。
Loader 会处理路径、类型、缓存和依赖。
回调不是无条件有效,必须检查错误和对象生命周期。
加载状态、请求去重和失败回退是工程必需品。十二、下一课预告
text
Lesson031|异步加载、预加载、进度和回调竞态