Files
aibis-dream/Docs/ResourceLoadingBestPractices.md

508 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 资源加载最佳实践指南
> 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 注意事项
**禁止在 Awake 中加载资源**`SceneResourceLoader``Awake` 中注册自身到 `ResourceSystem`,而 Unity 的 `Awake` 执行顺序是不确定的。如果在另一个组件的 `Awake` 中调用 `ResourceSystem.LoadAsync`,此时当前场景的 `SceneResourceLoader` 可能尚未完成注册,`ResourceSystem` 会因找不到 Scene Loader 而回退到 Persistent Loader 加载资源。这会导致:
1) 资源被错误地缓存到 Persistent Loader,永远不会随场景卸载释放;
2) 后续在协程中再次加载同 Key 资源时,由于缓存命中,返回的是 Persistent Loader 中的 Handle,进一步掩盖问题。
**解决方案**:所有资源加载必须在协程(如 `Start``IEnumerator` 流程)中执行,确保 `SceneResourceLoader.Awake()` 已完成执行。
- **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*