〇它是干什么的
先花 30 秒建立直觉,再动手写。
RevResManager.LoadAsync<Sprite>("UI/Bag", "icon_sword", sprite => img.sprite = sprite); // ① 异步加载(推荐) var prefab = RevResManager.Load<GameObject>("UI/Bag", "ItemSlot"); // ② 同步加载(小资源 / 已预热) RevResManager.Release("UI/Bag", "icon_sword"); // ③ 不用了释放(引用计数减一)
更省心的写法是用 作用域:using (RevResManager.OpenScope()) { … } —— 出块时这一批全放掉。
剩下的(预加载、按组卸载、句柄、缓存统计……)都是用到再看的增值项。
引用计数是核心:同一份资源被 3 个人用就记 3,谁都不用(计数归零)才会被卸载 —— 所以你拿一次就要还一次,这一条记住就不会泄漏。
| 它替你解决的问题 | 怎么做的 |
|---|---|
| 同一份资源被多处加载 | 内部共享缓存 + 引用计数:第二个人拿到的是同一份,不会加载两次 |
| 加载中又有人请求同一份 | 请求合并(回调排队),不会重复走 IO |
| 忘记卸载导致内存涨 | 计数归零即进"待卸载"名单;FlushUnused() 主动清、RevResAutoUnloader 也可自动清 |
| 编辑器里想直接跑、不想打包 | 编辑器直读模式:直接从 Assets 读,改完立刻生效 |
| 上线后要按包管理 | AB 模式:按 ResMap.txt 把逻辑路径映射到真实 AB |
| 失败不知道为什么 | 失败原因带枚举 RevResLoadErrorReason(路径没映射 / 包里没有 / 类型不符…) |
一3 分钟跑起来(可粘贴)
不用初始化、不用挂脚本。
// ① 异步加载一张图(最常见的用法) void Start() { RevResManager.LoadAsync<Sprite>("UI/Bag", "icon_sword", sprite => { image.sprite = sprite; // 回调里直接用 // 这个界面关掉时记得:RevResManager.Release("UI/Bag", "icon_sword"); }); } // ② 加载预制体并实例化 RevResManager.LoadAsync<GameObject>("UI/Bag", "ItemSlot", prefab => { Instantiate(prefab, container); }); // ③ 一批资源一起用、一起放(作用域,最省心) using (RevResManager.OpenScope()) { var bg = RevResManager.Load<Sprite>("UI/Common", "bg_main"); var ico = RevResManager.Load<Sprite>("UI/Common", "ico_coin"); // …这一段里用它们… } // 出块:上面两个资源自动 Release // ④ 看一眼当前缓存了多少(排查用) Debug.Log($"缓存 {RevResManager.CachedCount} 份,其中待卸载 {RevResManager.UnusedCount} 份");
Revolution.Tools / 资源 / RevAB 打包工具 → 「打包」页签 → 资源目录)。
二"我要做 X" 对照表(全部 API)
左边找需求,右边抄一行。入口统一是 RevResManager。
| 我想… | 这么写 |
|---|---|
| 异步加载(推荐) | RevResManager.LoadAsync<T>(rootPath, resName, obj => { }) |
| 同步加载 | RevResManager.Load<T>(rootPath, resName) |
| 拿句柄(自己管引用) | RevResManager.LoadHandle<T>(rootPath, resName) |
| 查已在缓存里的 | RevResManager.Get(rootPath, resName)(没有就是 RevResHandle.Empty) |
| 释放一个 | RevResManager.Release(rootPath, resName) |
| 一批一起放(推荐) | using (RevResManager.OpenScope()) { … } |
| 手动加减引用 | RevResManager.AddRef(key) / RevResManager.DecRef(key)(key 从句柄拿) |
| 立刻清掉没人用的 | RevResManager.FlushUnused() |
| 按分组卸载 | RevResManager.UnloadGroup(RevResGroup.Config)(force: true 连在用的一起卸) |
| 看缓存状况 | RevResManager.CachedCount / RevResManager.UnusedCount / RevResManager.CachedHandles |
| 接自己的加载策略 | RevResManager.RegisterPolicy(policy)(进阶:实现 IRevResPolicy) |
| 自定义资源分组 | RevResGroupUtil.Custom(0)(传 0/1/2… 得到不同分组) |
| 让资源从 Resources 文件夹加载 | RevResManager.Load<Sprite>("Res/UI/Bag", "icon_sword")(Res/ 前缀走 Resources 通道,见第五节) |
LoadAsync + Release 就够了 —— 别为了"看起来高级"去手动 AddRef/DecRef。
三路径怎么给(唯一需要记的约定)
永远是两段:rootPath(目录)+ resName(文件名)。
| 实际文件 | 这么写 | 说明 |
|---|---|---|
Assets/GameRes/UI/Bag/icon_sword.png | ("UI/Bag", "icon_sword") | 相对资源根目录(打包配置里那个) |
…/UI/Bag/ItemSlot.prefab | ("UI/Bag", "ItemSlot") | 不带扩展名 |
…/Config/Hero.txt | ("Config", "Hero") | 配置表也走同一套(见《配置表 / 导表工具》) |
| 按包分组 | ("UI/Bag", "icon_sword", RevResGroup.UI) | 分组影响 AB 归属与批量卸载 |
("", "Assets/GameRes/UI/Bag/icon_sword.png") ✗ —— 两段都要按上面来。② 写扩展名:
("UI/Bag", "icon_sword.png") ✗ —— 名字不带后缀。
四什么时候该释放
一句话:拿了几次就还几次;界面/场景销毁时一定要还。
using (RevResManager.OpenScope()),别手写 ReleaseOnClose / OnRelease 里 Release(面板生命周期见《UI 系统使用说明》)UnloadGroup(group) 一次收干净FlushUnused():只清计数为 0 的,绝对安全CachedCount 涨不涨;配合 RevAB 窗口的「体积」页签看谁大五两种加载模式(编辑器直读 / AB)
同一行业务代码,两种模式下都不用改 —— 但行为有两处差别要知道。
StreamingAssets 读(源码里就写着这句,见 RevABLoader.cs 头部注释)。
资源热更由官方扩展包 RevHotUpdate 提供:它通过本体预留的 2 个默认 null 钩子
(RevABLoader.BundlePathResolver = 包路径重定向 / RevResBootstrap.ResMapOverride = 映射表覆盖)
把加载指到持久化版本目录(Android / iOS / PC)或 CDN 的版本化 URL(小游戏)—— 不装 = 与从前逐字节一致。
装法:git clone -b hotupdate https://github.com/Yokino337088/Revolution.git Assets/Revolution.HotUpdate
(框架本体走 -b package);文档见 Revolution.Document/热更新/(使用说明 / 架构解析 / 技术方案)。
想自己接:资源层对外只有 IRevResPolicy + IRevResLoader 两个接口 ——
自己实现(接你的下载/CDN 体系)或整体换成 YooAsset,上层业务代码都不用改。
方案与步骤见仓库首页 README《不做什么:热更新与远程更新》:
GitHub 上打开 ·
在线文档站
Assets 直接读文件:改动立刻生效、不用打包。注意:不走 AB,所以"包归属""依赖是否被复制"这类问题它看不出来 —— 这类问题要用 RevAB 窗口的「依赖 / 体积 / 检查」页签看。
ResMap.txt(逻辑路径 → AB 与真实路径)加载。注意:改了资源或分包后,必须先回「打包」页签点一次「仅生成映射」,否则新资源在真机上会报"没映射"。
| 现象 | 原因与处理 |
|---|---|
| 编辑器里好好的,真机说找不到 | 没生成映射(ResMap.txt 里没有这条路径)→ 点「仅生成映射」再打包 |
| 路径对但类型不对(图拿成 Prefab) | 泛型参数写错:LoadAsync<Sprite> 与文件真实类型要一致 |
| 同一个资源在两个界面各加载一次 | 两段路径写法不一致(编辑器会当成两个资源)→ 统一约定(见第三节) |
Resources 兜底通道(资源直接放进 Resources 文件夹也能加载)
支持 ✓ —— 把资源丢进任意 Resources 文件夹就能通过框架加载 ✓。它是策略链的最后一环(RevResourcesResPolicy),接两类请求:
// ① 显式指定:逻辑路径最前面加 "Res/"(前缀必须落在整条路径最开头) // 文件:Assets/GameRes/Resources/UI/Bag/icon_sword.png var icon = RevResManager.Load<Sprite>("Res/UI/Bag", "icon_sword"); // ② AB 回落:普通逻辑路径,ResMap 查不到 / AB 加载失败时自动转到 Resources var icon2 = RevResManager.Load<Sprite>("UI/Bag", "icon_sword");
· 路径规则就是 Resources.Load 那一套:相对任意 Resources 文件夹、不带扩展名、层级一致 —— 上面那份文件对应的逻辑路径是 UI/Bag/icon_sword。
· 编辑器“开发模式”下这条通道不注册:默认只注册编辑器直读策略,AB 与 Resources 策略都不参与 → 编辑器里写 Res/xxx 反而会找不到 ✗。想在编辑器里验证:菜单 Revolution.Tools / 资源 / AB 加载模式(编辑器) 切到 AB 模式,或到真机验证 ✓。
· 代价:Resources 下的东西会被 Unity 无条件打进包体、且无法按需卸载 ✗ —— 它是“兜底 + 极小必需资源”的定位,别把大资源丢进去(框架自己只往里放一张 ResMap.txt)。
· 仍然别手写 Resources.Load ✗ —— 经框架走(RevResManager.Load / LoadAsync)才拿得到引用计数、分组卸载与失败原因 ✓。
六新手最容易踩的 6 个坑
| 坑 | 正确做法 |
|---|---|
| ① 加载了不释放,内存一直涨 | 要么 using (OpenScope()),要么在销毁时 Release —— 成对出现 |
② 把同步 Load 用在"大资源"上 | 大资源(模型、大图、场景)一律 LoadAsync,避免卡帧 |
| ③ 在回调里假设"马上就到" | 异步回调可能下一帧甚至更晚才来;回调里要判 this 是否已销毁(界面已关) |
| ④ 同一个资源 Release 两次 | 引用计数会变成负数(框架会告警但不背锅)→ 用作用域或配对管理 |
⑤ 手写 Resources.Load | 绕过框架就享受不到引用计数/映射/打包管理 ✗ → 一律 RevResManager |
⑥ 在编辑器里用 Res/ 前缀试 Resources 通道 | 编辑器“开发模式”只注册直读策略(AB / Resources 都不注册)→ 编辑器里测不通 ✗;切菜单「Revolution.Tools / 资源 / AB 加载模式(编辑器)」,或到真机上验证 ✓ |
七出问题怎么查 + 相关文档
3 分钟定位
- 看缓存:
RevResManager.CachedCount一直涨 → 有地方只拿不放(在界面关闭处找);UnusedCount一直不清 → 没人触发回收,调一次FlushUnused()看能不能清掉。 - 看映射:报"找不到"先在
Assets/Revolution/Resources/ResourceSystem/ResMap.txt里搜这条逻辑路径;搜不到就是没生成映射。 - 看分包:菜单
Revolution.Tools / 资源 / RevAB 打包工具→ 「分包」页签(带自动同步):确认资源有没有 AB 标记、「检查」页签有没有漏标。 - 看体积:「体积」页签找"哪个包突然变大";「依赖」页签看有没有资源被复制多份。
相关文档
- 《资源加载系统 · 架构解析》 —— 设计论证:为什么只走异步、引用计数怎么实现、AB 与直读如何切换、失败原因分类
- 文档总入口 · 公共 Mono 模块(每帧回调 / 协程宿主)· 对象池使用说明(与资源加载配合)