docs: 添加资源加载最佳实践文档

This commit is contained in:
2026-03-13 17:24:06 +08:00
parent 2d7b7ae9b0
commit 4cb39f6d2b
+501
View File
@@ -0,0 +1,501 @@
# 资源加载最佳实践指南
> AIBIS Dream 项目资源加载规范
---
## 快速参考表
| 场景 | 推荐方式 | 备注 |
|-----|---------|------|
| 场景初始化 | `ResourceSystem.LoadAsync` + yield return | 配合 PreloadByLabel 可减少 IO 等待 |
| 运行时加载 | `ResourceSystem.LoadAsync` | SceneResourceLoader 自动缓存同 Key 资源 |
| 常驻资源 | `ResourceSystem.LoadPersistentAsync` | 加载到 Persistent Loader,不随场景卸载 |
| Timeline 等大资源 | `LoadUncachedAsync` + `EarlyRelease` | 播放完立即释放 |
| 配置数据 | `PreloadByLabel` 批量预加载 | 在 Loading 界面期间完成,预热 Bundle 缓存 |
| 禁止 | `Resources.Load` / 直接调用 `Addressables` | 统一走 ResourceSystem |
---
## 1. 基本原则
### 1.1 统一入口
所有资源加载通过 `ResourceSystem` 静态门面,禁止直接使用 `Resources.Load``Addressables` API。
**例外:场景加载/卸载**`SceneLoader` 内部直接调用 `Addressables.LoadSceneAsync` / `UnloadSceneAsync`,因为场景操作有独立的生命周期语义(返回 `SceneInstance`),不适合纳入通用资源缓存体系。除此之外的资源加载均应走 `ResourceSystem`
旧的 `ResourceKit` 仅在过渡期保留,新代码不应使用。
### 1.2 异步优先
运行时资源加载使用异步方式(协程 yield return 或回调),避免主线程阻塞。
### 1.3 场景作用域管理
资源 Handle 由 `SceneResourceLoader` 统一持有,场景卸载时自动批量释放。开发者无需手动管理大多数资源的生命周期。
### 1.4 两级 Loader
- **Persistent Loader**:挂在 Persistence 场景,管理全局常驻资源(Actor prefab、通用 UI 等),游戏退出时释放
- **Scene Loader**:每个业务场景各一个,场景卸载前由 `SceneLoader` 主动释放
### 1.5 大资源提前释放
Timeline 等大资源应使用 `LoadUncachedAsync` 加载,播放完后通过 `EarlyRelease` 立即释放,不必等到场景卸载。
---
## 2. API 使用指南
### 2.1 三种路由方式
| 方法 | 目标 Loader | 适用场景 |
|-----|-----------|---------|
| `ResourceSystem.LoadAsync<T>(key)` | 自动(场景优先,回退持久化) | 大部分业务代码的默认选择 |
| `ResourceSystem.LoadPersistentAsync<T>(key)` | 持久化 Loader | Actor prefab、Bubble prefab 等常驻资源 |
| `ResourceSystem.LoadSceneAsync<T>(key)` | 场景 Loader | 明确只属于当前场景的资源 |
### 2.2 注意事项
**ActiveLoader 回退风险**`LoadAsync`(自动路由)在 Scene Loader 不存在时会静默回退到 Persistent Loader。这意味着在场景切换间隙(旧场景已卸载、新场景 `SceneResourceLoader.Awake` 尚未执行)调用 `LoadAsync`,资源会被加载到 Persistent Loader 中,**永远不会随场景卸载释放**。避免在此时间窗口内调用 `LoadAsync`;若有明确场景归属,优先使用 `LoadSceneAsync``LoadPersistentAsync`
**同一 Key 只能以一种类型加载**`SceneResourceLoader` 的缓存以 Key 字符串为索引。如果先调用 `LoadAsync<Sprite>(key)` 再调用 `LoadAsync<Texture2D>(key)`,第二次会返回已缓存的 Handle 并尝试类型转换,可能导致 `InvalidCastException`。确保同一 Key 在整个场景生命周期内只以一种类型加载。
### 2.3 协程风格(推荐)
```csharp
IEnumerator Example()
{
var handle = ResourceSystem.LoadAsync<Sprite>("Memory/mem_001");
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded)
_image.sprite = handle.Result;
}
```
### 2.4 回调风格
```csharp
ResourceSystem.LoadAsync<Sprite>("Memory/mem_001",
sprite => _image.sprite = sprite,
error => Debug.LogError(error));
```
### 2.5 Timeline 等大资源
```csharp
// 加载(不走缓存,获得独立 Handle)
var handle = ResourceSystem.CurrentSceneLoader.LoadUncachedAsync<PlayableAsset>(timelineKey);
yield return handle;
director.playableAsset = handle.Result;
// 播放完后立即释放
ResourceSystem.EarlyRelease(handle);
```
### 2.6 批量预加载
```csharp
var handle = ResourceSystem.PreloadByLabel<Sprite>("scene_clinic");
yield return handle;
// PreloadByLabel 会预热 Addressables 底层 Bundle 缓存(Bundle 已加载到内存),
// 后续 LoadAsync 同 Bundle 内的资源时无需再等 IO,但仍会经过一次 Addressables
// 内部解析。注意:预加载的结果不会写入 SceneResourceLoader 的 _cache 字典,
// 后续 LoadAsync 会创建新的 Handle 并缓存。
```
---
## 3. SceneResourceLoader 配置
在场景中挂载 `SceneResourceLoader` 组件:
- **Persistence 场景**:勾选 `isPersistent`,在 Awake 中自动注册为 Persistent Loader
- **业务场景**:不勾选 `isPersistent`,在 Awake 中自动注册为 Scene LoaderOnDestroy 时自动注销
`SceneLoader` 会在场景卸载前主动调用 `ReleaseAll()`,确保在 `UnloadSceneAsync` 之前完成资源释放,避免 OnDestroy 执行顺序不确定的问题。
**ReleaseAll 幂等性**`SceneLoader` 先显式调用 `ReleaseAll()`,随后场景卸载时 `SceneResourceLoader.OnDestroy()` 会再次调用。这是安全的——第二次调用时 `_cache``_uncachedHandles` 已被清空,不会重复释放。这一设计是有意为之:显式调用保证时序正确,OnDestroy 兜底保证即使漏调也不泄漏。
---
## 4. Addressable 命名与分类规范
> **核心思路**
>
> - **Group** 对齐加载生命周期 — 同一场景/同一时机加载的资源放进同一个 Group(= 同一个 Bundle)。
> - **Key** 保证可预测可构造 — 固定 `Category/Path` 格式,代码中可拼接得出。
> - **Label** 仅用于跨 Group 聚合 — 当预加载批次横跨多个 Group 时才创建 Label;若某场景的动态资源全在一个 Group 内,不需要额外 Label。
---
### 4.1 Addressable Group
#### 命名格式
`{分类}_{PascalCase标识}`
#### 分类体系
| 分类 | 命名格式 | 说明 |
|------|----------|------|
| 核心常驻 | `Core` | 游戏全程常驻的资源,对应 Persistent Loader |
| 场景专属 | `Scene_{场景名}` | 某场景独占的资源,场景卸载时释放 |
| 功能模块 | `Feature_{模块名}` | 可在多个场景复用的游戏功能模块资源 |
| 共享资源 | `Shared_{类别名}` | 多场景共用、按需加载的资源 |
| 本地化 | `Localization-*` | 由 Localization 包自动管理,不手动修改 |
| 内建 | `Built In Data` | Unity 系统管理,不手动修改 |
#### 分组原则
1. **按加载时机而非资源类型分组**:同一场景的动画、精灵、Timeline 放在同一个 Group,而非按类型拆分到不同 Group。这样一次 IO 即可加载该场景所需的全部资源。
2. **从不同时加载的资源必须分开**:例如火山场景、酒吧场景、梦境场景不要放进同一个 Group,否则进入火山时会加载酒吧和梦境的无用资源。
3. **可复用的玩法逻辑资源独立为 Feature 组**:如 `BlockPuzzle/BlockShape` Prefab 可能在多个维修场景复用,放入 `Feature_BlockPuzzle`;但该模块在石头维修场景中使用的缩略图、部件图等美术资源,放入 `Scene_StoneFix`
#### 当前项目分组方案
| Group | 包含内容 | 依据 |
|-------|----------|------|
| `Core` | ActorAnima/ActorAnimaEx 等常驻 Prefab、BubbleSprite、SceneSO、PipelinePrefab | 在 Persistence 场景加载后常驻,全程不释放 |
| `Scene_ClinicOut` | ClinicOut 场景、诊所外角色动画、Mobile 对象 | 主场景,进入频率最高,资源量适中 |
| `Scene_SubWay` | SubWay/Bridge 场景及窗外背景 | 地铁+天桥紧密关联,总是一起出现 |
| `Scene_PeipeiFix` | PeipeiFixScene、OpenFixScene、佩佩动画/精灵 | 佩佩维修线专属 |
| `Scene_StoneFix` | ExpressFixScene、石头动画、BlockPuzzle 美术资源(缩略图/部件图)、石头线 Timeline | 石头维修线专属;BP 美术只在此场景使用 |
| `Scene_HuoShanFix` | HuoShanFixScene 及火山专属资源 | 火山维修线专属 |
| `Scene_Bar` | 酒吧场景及专属角色动画 | 酒吧场景专属 |
| `Scene_Dream` | 梦境场景及专属资源 | 梦境场景专属 |
| `Shared_Actor` | 在多个场景出现的角色动画(如医生在诊所外/酒吧/地铁均出现) | 不属于单一场景,按需加载 |
| `Shared_CG` | CG/插图(由 Yarn 对话触发,可能跨场景使用) | 体积较大,独立管理 |
| `Feature_BlockPuzzle` | BlockPuzzle/BlockShape Prefab、BlockPuzzleValidator | 纯玩法逻辑资源,可跨场景复用 |
| `Feature_WhackMole` | WhackMoleData | 打地鼠关卡数据 |
---
### 4.2 Addressable Key
#### 格式
```
{Category}/{Path}
```
- **Category**:资源类型前缀,PascalCase
- **Path**:资源路径,PascalCase,用 `/` 分隔层级,不超过 3 级
**例外 — 场景 Key 使用裸名**:场景通过 `Addressables.LoadSceneAsync` 加载,Key 直接使用 PascalCase 场景名,不加类型前缀。原因:场景名被 `TalkSceneSO.firstSceneName`、Yarn `<<load_scene>>` 命令、`GameManager` 硬编码等深度引用,保持裸名是最务实的选择。
#### Category 列表
| Category | 资源类型 | Key 示例 | 代码构造方式 |
|----------|----------|----------|-------------|
| `Animation` | AnimatorController / OverrideController | `Animation/Peipei` | `$"Animation/{actorName}"` |
| `AnimationConfig` | 动画配置 ScriptableObject | `AnimationConfig/Peipei` | `$"AnimationConfig/{actorName}"` |
| `Sprite` | 精灵图(含子对象) | `Sprite/Peipei[Normal]` | `$"Sprite/{actorName}[{state}]"` |
| `CG` | CG / 插图 | `CG/Opening1` | `$"CG/{imagePath}"` |
| `Timeline` | PlayableAsset | `Timeline/BlockPuzzle/PanelOpen` | `$"Timeline/{module}/{action}"` |
| `BubbleSprite` | 对话气泡精灵 | `BubbleSprite/Normal` | `$"BubbleSprite/{name}"` |
| `Prefab` | 预制体 | `Prefab/ActorAnima` | `$"Prefab/{name}"` |
| `BlockPuzzle` | 拼图专属资源 | `BlockPuzzle/BlockShape` | `$"BlockPuzzle/{path}"` |
| `WhackMole` | 打地鼠数据 | `WhackMole/Level1` | `$"WhackMole/{levelName}"` |
| `Mobile` | 移动物体 | `Mobile/Car` | `$"Mobile/{name}"` |
| `Config` | 配置数据 / SO | `Config/SceneSO` | 直接引用 |
#### 命名规则
| 规则 | 正确 | 错误 | 依据 |
|------|------|------|------|
| Key 允许中英文混合 | `Animation/Peipei``Animation/石头` | — | 团队习惯优先,UTF-8 编码无技术障碍 |
| 角色名用英文代号 | `Animation/ClinicOutPeipei` | `Animation/诊所外佩佩` | 统一使用项目角色代号 |
| 不含空格 | `Prefab/TaskItem` | `Task Item Temp` | 空格在 URL、命令行中易出错 |
| 不含文件扩展名 | `WhackMole/Level1` | `WhackMoleData/Level1.asset` | Key 是逻辑地址,不是文件路径 |
| 层级不超过 3 级 | `Timeline/BlockPuzzle/PanelOpen` | `Timeline/Fix/Stone/BP/PanelOpen` | 层级过深增加拼接复杂度 |
| 子对象用 `[]` 语法 | `Sprite/Peipei[Normal]` | `Sprite/Peipei/Normal` | Addressables 原生子对象语法 |
| 场景 Key 裸名 PascalCase | `ClinicOut` | `Scene/ClinicOut` | 兼容 TalkSceneSO 和 LoadSceneAsync |
#### 角色命名参考
中英文均可,建议同一类别保持一致:
| 中文 | 英文 |
|------|------|
| 佩佩 | Peipei |
| 石头 | Stone |
| 火山 | HuoShan |
| 医生 | Doctor |
| 英理 | Yingli |
| 调酒妹 | Bartender |
**命名建议**:若某类别已存在中文命名(如 `Animation/石头`),新增资源保持中文风格(`Animation/医生`);若已存在英文命名,保持英文风格。不要在同一类别中混用。
#### Timeline Key 约定
`DirectorHandler` 对 Timeline Key 会自动添加 `Timeline/` 前缀。传入的 ref 格式为 `{Module}/{Action}`,实际 Addressable Key 为 `Timeline/{Module}/{Action}`
| Module | 中文对应 | Key 示例 |
|--------|----------|----------|
| `Snap` | 卡扣 | `Timeline/Snap/Lock`, `Timeline/Snap/Unlock` |
| `BlockPuzzle` | 引擎仓面板 | `Timeline/BlockPuzzle/PanelOpen` |
| `Cable` | 插线面板 | `Timeline/Cable/PanelOpen` |
| `Engine` | 引擎模块 | `Timeline/Engine/Open` |
| `Clinic` | 维修间 | `Timeline/Clinic/Enter` |
---
### 4.3 Addressable Label
#### 设计原则
- **Group** 决定打包粒度(哪些资源进同一个 Bundle)。
- **Label** 决定预加载批次(哪些资源在同一时机一起预热)。
- **Label 的价值在于横切 Group 边界**:把来自不同 Bundle、但需要在同一时刻预热的资源聚合起来。
- 若某场景的全部动态资源恰好都在同一个 Group 内,**不需要**为它创建 Label。
#### Label 格式
全小写,单词间用连字符连接。仅在确实需要跨 Group 聚合时才创建。
#### 当前项目 Label
| Label | 横切的 Group | 用途 | 预加载时机 |
|-------|-------------|------|-----------|
| `persistent` | `Core` + `Shared_Actor`(常驻角色) | 启动时预加载所有常驻资源 | 游戏启动 Loading |
| `fix-common` | `Core`(维修通用 Timeline+ 各 `Scene_*Fix` 共用的维修 UI | 进入任意维修场景前预加载通用维修资源 | 维修场景 Loading |
| `preload:stone-fix` | `Scene_StoneFix` + `Feature_BlockPuzzle` | 石头维修的动态资源跨两个 Group | 进入 ExpressFixScene 前 |
| `preload:huoshan-fix` | `Scene_HuoShanFix` + `Feature_WhackMole`(如有) | 火山维修的动态资源可能跨 Group | 进入 HuoShanFixScene 前 |
| `SceneSO` | `Core`(保持兼容) | `GameManager` 启动时用 `LoadAssetsAsync("SceneSO")` 加载所有章节配置 | 游戏启动 |
#### 什么时候不需要 Label
| 场景 | 原因 |
|------|------|
| ClinicOut | 动态资源全在 `Scene_ClinicOut` 一个 Group 内 |
| SubWay / Bridge | 动态资源全在 `Scene_SubWay` 内 |
| Dream | 动态资源全在 `Scene_Dream` 内 |
| Bar | 动态资源全在 `Scene_Bar` 内 |
这些场景若需要预加载,直接在 `SceneLoader.PreloadSceneAssets` 中按 Key 列表逐个加载即可,无需 Label。
---
### 4.4 Key / Label / Group 对照表
| 用途 | Key | Group | Label(仅跨 Group 时) |
|------|-----|-------|----------------------|
| 常驻 Actor Prefab | `Prefab/ActorAnima`(旧:`ActorAnima` | `Core` | `persistent` |
| 对话气泡精灵 | `BubbleSprite/Normal`(旧:`BubbleSprite/大对话框` | `Core` | `persistent` |
| 章节配置 | `Config/SceneSO`(旧:`SceneSO` | `Core` | `SceneSO`(兼容) |
| 诊所外场景 | `ClinicOut` | `Scene_ClinicOut` | — |
| 诊所外角色动画 | `Animation/ClinicOutPeipei` | `Scene_ClinicOut` | — |
| 地铁场景 | `SubWay` | `Scene_SubWay` | — |
| 佩佩维修场景 | `PeipeiFixScene` | `Scene_PeipeiFix` | — |
| 佩佩动画 | `Animation/Peipei` | `Scene_PeipeiFix` | — |
| 石头维修场景 | `ExpressFixScene` | `Scene_StoneFix` | `preload:stone-fix` |
| 石头动画 | `Animation/Stone` | `Scene_StoneFix` | `preload:stone-fix` |
| 石头线 Timeline | `Timeline/BlockPuzzle/PanelOpen` | `Scene_StoneFix` | `preload:stone-fix` |
| BP Prefab | `BlockPuzzle/BlockShape` | `Feature_BlockPuzzle` | `preload:stone-fix` |
| BP Validator | `BlockPuzzle/Validator` | `Feature_BlockPuzzle` | `preload:stone-fix` |
| 打地鼠数据 | `WhackMole/Level1` | `Feature_WhackMole` | `preload:huoshan-fix`(如有) |
| 跨场景角色(医生) | `Animation/BarDoctor` | `Shared_Actor` | `persistent` 或按需 |
| CG 插图 | `CG/Opening1` | `Shared_CG` | — |
---
### 4.5 历史资源兼容策略
#### 现状问题
- **裸 Key**`ActorAnima``NormalBubble``ClinicOut``BlockPuzzleValidtor`
- **含空格**`Task Item Temp``Screen Text`(需修正)
- **含扩展名**`WhackMoleData/xxx.asset`(需修正)
- **拼写错误**`ScriptalObjects``BlockPuzzleValidtor`(需修正)
#### 迁移优先级
| 优先级 | 动作 | 涉及范围 | 依据 |
|--------|------|----------|------|
| **P0 立即** | 为资源打 Label,重组 Group | 仅改 Addressable 配置,零代码改动 | 立即启用预加载能力 |
| **P1 短期** | 新增资源严格按规范命名 Key | 新资源 | 防止问题扩大 |
| **P2 中期** | 迁移 ConstRef 中的裸 Key(添加 `Prefab/` 前缀) | ~15 个常量 + 引用处 | 改动集中在 ConstRef.cs |
| **无需迁移** | Timeline、Animation 的中文 Key | — | 规范已允许中文命名 |
| **无需迁移** | 场景裸 Key`ClinicOut``SubWay` 等) | — | 裸名本身符合场景 Key 规范 |
#### 迁移步骤
1. 重组 Group:将现有组重命名/合并为 `Core``Scene_*``Feature_*``Shared_*`
2. 打 Label:为需要跨 Group 预加载的资源打上 `persistent``fix-common``preload:*` 等 Label。
3. 新资源强制新规:所有新增资源必须使用 `Category/Path` 格式,禁止裸 Key。
4. 代码全面切到 `ResourceSystem` 后,再评估旧 Key 重命名。
### 4.6 中文 Key 使用建议
1. **同一类别保持一致**:若 `Animation/石头` 已存在,新增动画也用中文(`Animation/医生`),不要混用 `Animation/Doctor`
2. **避免特殊字符**:仅限中英文、数字、下划线、斜杠、方括号。避免空格、`!@#$%^&*()` 等符号
3. **代码中引用**:若某资源频繁在代码中引用,建议在 `ConstRef` 中定义英文常量,如 `ConstRef.TimelineSnapLock = "Timeline/卡扣锁定"`
4. **Group 命名保持英文**Bundle 文件名使用英文 Group 名,避免在某些文件系统/CI 环境出现编码问题
#### 示例与反例
**符合规范:**
- `Animation/Peipei` — 类型前缀 + 英文角色名
- `Timeline/BlockPuzzle/PanelOpen` — 不超过 3 级
- `Sprite/Peipei[Normal]` — 子对象用 `[]` 语法
- `ClinicOut` — 场景裸名,符合场景 Key 例外规则
- `WhackMole/Level1` — 不含扩展名
**反例:**
- `BlockShape` — 裸 Key,缺类型前缀
- `Task Item Temp` — 含空格(应改为 `TaskItemTemp`
- `WhackMoleData/Level1.asset` — 不应包含扩展名(应改为 `WhackMole/Level1`
- `Scene/ClinicOut` — 场景 Key 不需要前缀(直接用 `ClinicOut`
---
## 5. 性能优化建议
| 问题 | 后果 | 解决方案 |
|-----|------|---------|
| 同步加载大资源 | 卡顿 | 使用 `ResourceSystem.LoadAsync` |
| 重复加载相同资源 | 内存浪费 | `SceneResourceLoader` 自动缓存同 Key |
| 不释放 Handle | 内存泄漏 | 场景卸载时 Loader 自动批量释放 |
| 大资源持有到场景结束 | 内存浪费 | 用 `LoadUncachedAsync` + `EarlyRelease` |
| 混用多种加载方式 | 管理混乱 | 统一使用 `ResourceSystem` |
---
## 6. 错误处理与降级策略
### 6.1 协程风格的错误处理
```csharp
IEnumerator LoadWithFallback()
{
var handle = ResourceSystem.LoadAsync<Sprite>("Sprite/Peipei[Normal]");
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
_image.sprite = handle.Result;
}
else
{
Debug.LogError($"[MySystem] 加载失败: {handle.OperationException}");
_image.sprite = _fallbackSprite; // 使用预设的占位资源
}
}
```
### 6.2 处理原则
| 情况 | 策略 |
|-----|------|
| UI 图片加载失败 | 显示占位图/透明,不影响交互流程 |
| Prefab 加载失败 | 记录错误日志,跳过实例化,避免 NullReferenceException |
| Timeline 加载失败 | 跳过播放,直接推进到下一个流程节点 |
| 配置数据加载失败 | 视为致命错误,中断当前流程并提示 |
| 批量预加载部分失败 | 记录警告,允许后续按需加载时重试(Addressables 会重新发起请求) |
### 6.3 避免静默失败
`SceneResourceLoader` 内部已在加载失败时输出 `LogError`。业务代码不应忽略 `handle.Status`,至少要在失败时 log 上下文信息(哪个系统、为什么需要这个资源),方便排查。
---
## 7. Prefab 加载:LoadAsync vs InstantiateAsync
### 7.1 两种方式对比
| | `LoadAsync<GameObject>` + `Instantiate` | `Addressables.InstantiateAsync` |
|---|---|---|
| 加载结果 | 获得 Prefab 引用,需手动 Instantiate | 直接返回实例化的 GameObject |
| 引用计数 | 由 SceneResourceLoader 的 Handle 管理 | 由 Addressables 内部管理,需调用 `ReleaseInstance` |
| 多次实例化 | 加载一次 Prefab,多次 Instantiate(推荐) | 每次调用产生独立 Handle |
| 与本系统集成 | 完全兼容,走 ResourceSystem 统一管理 | 绕过 ResourceSystem,需自行管理释放 |
| 适用场景 | 大部分 PrefabUI 面板、Actor 等) | 不推荐,除非有特殊的引用计数需求 |
### 7.2 推荐用法
本项目统一使用 `LoadAsync<GameObject>` + 手动 `Instantiate`,配合 `SceneResourceLoader` 缓存和自动释放:
```csharp
IEnumerator SpawnActor()
{
var handle = ResourceSystem.LoadPersistentAsync<GameObject>("Prefab/ActorAnima");
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
var instance = Object.Instantiate(handle.Result, parent);
// Prefab Handle 由 Persistent Loader 管理,无需手动释放
// 实例化的 GameObject 需在不用时 Destroy
}
}
```
不推荐直接使用 `Addressables.InstantiateAsync`,因为它绕过了 `ResourceSystem` 的统一管理。
---
## 8. 调试与内存监控
### 8.1 运行时查看资源持有量
`SceneResourceLoader` 提供了以下属性,可在 Inspector 或自定义调试面板中查看:
| 属性 | 含义 |
|-----|------|
| `CachedCount` | 当前缓存的资源 Handle 数量(按 Key 去重) |
| `UncachedCount` | 未缓存的独立 Handle 数量(LoadUncachedAsync / PreloadByLabel |
| `TotalHandleCount` | 总持有 Handle 数 |
可在开发阶段写一个简单的 Debug UI 显示这些数值:
```csharp
void OnGUI()
{
var scene = ResourceSystem.CurrentSceneLoader;
var persistent = ResourceSystem.PersistentLoader;
GUILayout.Label($"Scene: {scene?.TotalHandleCount ?? 0} handles");
GUILayout.Label($"Persistent: {persistent?.TotalHandleCount ?? 0} handles");
}
```
### 8.2 Addressables Event Viewer
Unity 编辑器中可通过 **Window > Asset Management > Addressables > Event Viewer** 查看:
- 每个 Handle 的加载/释放时间线
- 当前活跃的 Handle 数量
- Bundle 加载/卸载事件
需要在 **AddressableAssetSettings** 中启用 **Send Profiler Events** 选项。
### 8.3 常见泄漏排查
| 现象 | 可能原因 | 排查方式 |
|-----|---------|---------|
| 场景切换后内存不降 | 资源被加载到 Persistent LoaderActiveLoader 回退) | 检查 `PersistentLoader.CachedCount` 是否异常增长 |
| 同一场景内存持续增长 | 重复调用 `LoadUncachedAsync` 未释放 | 检查 `UncachedCount` 是否持续增长 |
| 退出场景后 Addressables 报 Handle 泄漏警告 | `SceneResourceLoader` 未被正确销毁 | 确认场景中挂载了 `SceneResourceLoader``SceneLoader` 正确调用了卸载流程 |
---
## 9. 代码审查清单
- [ ] 没有使用 `Resources.Load`
- [ ] 没有直接调用 `Addressables` API(应通过 ResourceSystem,场景加载除外)
- [ ] 运行时资源加载使用异步方式
- [ ] 加载结果检查了 `handle.Status`,失败时有降级处理或日志(§6
- [ ] Timeline 等大资源使用 `LoadUncachedAsync` + `EarlyRelease`
- [ ] 常驻资源通过 `LoadPersistentAsync` 加载
- [ ] Prefab 使用 `LoadAsync<GameObject>` + `Instantiate`,不使用 `InstantiateAsync`(§7
- [ ] 同一 Key 未以不同类型重复加载(§2.2)
- [ ] 新增 Addressable Key 符合 `Category/Path` 命名规范(§4.2
- [ ] 新增资源放入了正确的 Addressable Group(§4.1
- [ ] 跨 Group 预加载的资源已打上对应 Label(§4.3)
- [ ] 业务场景中挂载了 `SceneResourceLoader` 组件
---
*文档版本: 3.1*
*更新日期: 2026-03-11*