Skip to content

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 不检查资源类型和有效状态 ​

类型、生命周期和请求版本同样重要。


十、练习与答案 ​

练习 ​

  1. 动态加载的高层步骤是什么?
  2. 为什么加载 SpriteFrame 可能还要处理 Texture?
  3. 如何防止旧页面的异步回调更新新页面?
  4. 重复加载同一资源时,Loader 可能如何处理?

参考答案 ​

  1. 解析路径、查缓存、加载依赖和本体、解码创建对象、回调通知。
  2. SpriteFrame 可能依赖 Texture 或 Atlas。
  3. 使用对象有效性、请求 ID 或页面生命周期状态检查。
  4. 命中缓存或复用进行中的请求,不一定重新读取。

Creator 2.4.x 编辑器实验:亲眼观察异步、缓存和类型 ​

使用 Creator 编辑器创建资源、节点和组件绑定。不要手工编辑 .scene、.prefab、.meta 或生成目录。

实验准备 ​

  1. 在 Creator 资源管理器中创建 assets/resources/load-lab/。
  2. 导入一张小图片,命名 icon-a.png,等待 AssetDB 完成。
  3. 创建 ResourceLoadLab.scene,放置一个 Sprite 和三个 Button。
  4. 创建 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 随意释放共享头像。

深化练习 ​

  1. 为什么缓存命中也不能允许调用方在 load 下一行读取结果?
  2. 一个页面同时请求 A、B、C 三张图,C 先完成是否代表加载系统乱序?
  3. 如何区分“资源回调完成”和“画面已经渲染出首帧”?
  4. 路径正确、无 err、Sprite 仍为空,应继续检查哪些对象层次?

推导答案 ​

  1. API 契约是异步回调,缓存只改变内部耗时,不应改变调用方控制流,否则同一代码在冷缓存和热缓存下会有两套时序。
  2. 不代表。异步任务按缓存、依赖、大小和 I/O 状态各自完成,提交顺序不是完成顺序;业务如需顺序,应自己建立聚合或状态机。
  3. 回调说明 Asset 可交给业务;赋给组件后还要经过组件更新、渲染数据生成和下一帧提交。应分别记录 load callback、赋值时刻和下一帧可见/渲染指标。
  4. 检查返回类型、target Sprite 是否有效、节点是否 active、SpriteFrame 是否真正赋值、材质/颜色/opacity、节点尺寸和层级,而不是再次 load。

十一、本课总结 ​

text
load API 是异步资源流程的入口。
Loader 会处理路径、类型、缓存和依赖。
回调不是无条件有效,必须检查错误和对象生命周期。
加载状态、请求去重和失败回退是工程必需品。

十二、下一课预告 ​

text
Lesson031|异步加载、预加载、进度和回调竞态