AlicizaX/Client/Assets/Books/Framework/Runtime/Resource.md

674 lines
16 KiB
Markdown
Raw Permalink Normal View History

2026-04-01 13:20:06 +08:00
# Resource
## 模块概述
Resource 模块基于 YooAsset 封装了资源包初始化、版本管理、资源查询、同步/异步加载、实例化、缓存回收与低内存处理能力。
它是多个模块的底层依赖:
- `UI`:加载窗口和 Widget 预制体
- `Audio`:加载音频资源
- `Localization`:加载语言表
- `GameObjectPool`:加载池对象原始 prefab
- `Scene`:主场景与附加场景的资源加载链路
从当前实现来看,`ResourceService` 还额外提供了:
- 多包管理
- 同一路径并发加载合并
- 资源对象池缓存
- 低内存时统一回收入口
- 下载、版本、清单更新能力
## 快速开始
### 最少步骤
1. 在场景中挂载 `ResourceComponent`
2. 配置 `PlayMode`、默认包名和解密服务名
3. 游戏启动时调用 `InitPackageAsync`
4. 通过 `GameApp.Resource``AppServices.Require<IResourceService>()` 加载资源
### 最小示例
```csharp
using AlicizaX;
using UnityEngine;
public sealed class ResourceQuickStart : MonoBehaviour
{
private async void Start()
{
await GameApp.Resource.InitPackageAsync();
Texture2D icon = await GameApp.Resource.LoadAssetAsync<Texture2D>("UI/Common/Icon");
Debug.Log(icon != null);
}
}
```
## 架构说明
```text
ResourceComponent
└─ ResourceService
├─ YooAsset PackageMap
├─ AssetInfo Cache
├─ AssetObject Pool
├─ Loading Operation Map
├─ Download / Version / Manifest
└─ ResourceExtComponent / AssetsReference
```
### 关键协作关系
- `ResourceComponent`:负责注册服务、配置运行模式并接管低内存回收节奏
- `ResourceService`:负责包初始化、加载、缓存和释放
- `AssetsReference`:负责把实例对象与源资源句柄关联起来,在对象销毁时自动释放源资源引用
- `ResourceExtComponent`:负责追踪资源绑定目标并分帧回收无引用资源
### 运行模式
当前实现支持以下 `PlayMode`
- `EditorSimulateMode`
- `OfflinePlayMode`
- `HostPlayMode`
- `WebPlayMode`
不同模式决定初始化参数:
- **EditorSimulateMode**:编辑器模拟构建目录
- **OfflinePlayMode**:本地内置资源
- **HostPlayMode**:内置资源 + 缓存 + 远端下载
- **WebPlayMode**Web 环境资源拉取方式
### 并发加载合并机制
`ResourceService` 内部使用 `_assetLoadingOperations` 合并同一路径的并发加载请求。
这意味着:
- 多个系统同时加载同一个资源时,不会重复发起多次底层加载
- 后来的请求会等待第一条加载链路完成,再复用结果
这对 UI 和公共图集资源特别有价值。
## 核心类与接口
### `IResourceService`
该接口同时覆盖:
- 包初始化
- 包版本与清单管理
- 资源存在性检查
- 同步/异步加载
- 场景对象实例化
- 缓存清理
- 低内存回收
### `ResourceComponent`
它是 Resource 模块在场景中的入口组件,主要职责:
- 注册 `ResourceService`
- 配置默认包名 `PackageName`
- 设置运行模式 `PlayMode`
- 设置解密服务名 `decryptionServices`
- 设置资源对象池参数
- 接管 `Application.lowMemory` 事件
### `ResourceService`
从当前实现看,`ResourceService` 维护以下关键状态:
- `DefaultPackageName`
- `PlayMode`
- `PackageMap`
- `_assetInfoMap`
- `_assetLoadingOperations`
- `_assetPool`
### `AssetsReference`
这是实例对象与源资源句柄的桥接组件。
作用:
- 当你通过 `LoadGameObject` / `LoadGameObjectAsync` 实例化资源时
- 框架会在实例对象上自动挂一个 `AssetsReference`
- 当该实例对象销毁时,对应源资源引用会自动 `UnloadAsset`
这就是为什么:
- `LoadGameObject` 返回的实例对象通常不需要你手动 `UnloadAsset`
### `ResourceExtComponent`
主要用于:
- 追踪已绑定给 UI、Sprite 或其他目标对象的资源
- 自动清理已经失去引用的资源包装对象
- 以分帧方式做回收,降低一次性遍历成本
## API 参考
以下内容按使用频率分组说明。
### 一、初始化与运行配置
#### `void Initialize()`
- 参数:无
- 返回值:无
- 说明:初始化 YooAsset、默认包与资产池
- 备注:通常由 `ResourceComponent.Start()` 自动调用
#### `UniTask<bool> InitPackageAsync(string packageName = "", string hostServerURL = "", string fallbackHostServerURL = "")`
- 可选参数:`packageName`
- 可选参数:`hostServerURL`
- 可选参数:`fallbackHostServerURL`
- 返回值:`UniTask<bool>`
说明:
- 初始化指定资源包
- 如果 `packageName` 为空,则使用 `DefaultPackageName`
- 在 Host/Web 模式下通常需要传入远端地址
异常与边界:
- 如果同一个包正在初始化或已成功初始化,会记录错误并直接返回失败结果
- 初始化失败时任务会抛出异常
#### `string DefaultPackageName { get; set; }`
- 说明:默认资源包名
- 推荐:整个项目统一默认包名约定
#### `EPlayMode PlayMode { get; set; }`
- 说明:资源系统运行模式
- 注意Editor 与真机配置往往不同
#### `string DecryptionServices { get; set; }`
- 说明:解密服务类型名
- 注意:该值应是可被反射创建的类型全名
#### `bool AutoUnloadBundleWhenUnused { get; set; }`
- 说明:初始化参数中的自动卸载选项
### 二、版本与下载管理
#### `string GetPackageVersion(string customPackageName = "")`
- 可选参数:`customPackageName`
- 返回值:包版本字符串
#### `RequestPackageVersionOperation RequestPackageVersionAsync(bool appendTimeTicks = false, int timeout = 60, string customPackageName = "")`
- 可选参数:`appendTimeTicks`
- 可选参数:`timeout`
- 可选参数:`customPackageName`
- 返回值:`RequestPackageVersionOperation`
#### `UpdatePackageManifestOperation UpdatePackageManifestAsync(string packageVersion, int timeout = 60, string customPackageName = "")`
- 必填参数:`packageVersion`
- 可选参数:`timeout`
- 可选参数:`customPackageName`
- 返回值:`UpdatePackageManifestOperation`
#### `ResourceDownloaderOperation CreateResourceDownloader(string customPackageName = "")`
- 可选参数:`customPackageName`
- 返回值:`ResourceDownloaderOperation`
说明:
- 会使用当前 `DownloadingMaxNum``FailedTryAgain`
### 三、资源查询
#### `HasAssetResult HasAsset(string location, string packageName = "")`
- 必填参数:`location`
- 可选参数:`packageName`
- 返回值:`HasAssetResult`
说明:
- 用于判断资源是否存在、是否在本地、是否需要远端下载
注意:
- 如果 `location` 为空会抛异常
#### `bool CheckLocationValid(string location, string packageName = "")`
- 必填参数:`location`
- 可选参数:`packageName`
- 返回值:`bool`
#### `AssetInfo GetAssetInfo(string location, string packageName = "")`
- 必填参数:`location`
- 可选参数:`packageName`
- 返回值:`AssetInfo`
说明:
- 内部带有 `_assetInfoMap` 缓存
- 同一路径重复获取成本较低
### 四、同步加载
#### `T LoadAsset<T>(string location, string packageName = "") where T : UnityEngine.Object`
- 必填参数:`location`
- 可选参数:`packageName`
- 返回值:`T`
- 泛型约束:`T : UnityEngine.Object`
说明:
- 同步加载资源
- 首次加载会进入资源池缓存
- 重复获取相同资源时会优先从 `_assetPool` 中复用
#### `GameObject LoadGameObject(string location, Transform parent = null, string packageName = "")`
- 必填参数:`location`
- 可选参数:`parent`
- 可选参数:`packageName`
- 返回值:`GameObject`
说明:
- 同步加载 prefab 并实例化
- 实例对象会自动挂 `AssetsReference`
- 销毁实例时会自动释放源资源引用
### 五、异步加载
#### `UniTask<T> LoadAssetAsync<T>(string location, CancellationToken cancellationToken = default, string packageName = "") where T : UnityEngine.Object`
- 必填参数:`location`
- 可选参数:`cancellationToken`
- 可选参数:`packageName`
- 返回值:`UniTask<T>`
#### `UniTask<GameObject> LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default, string packageName = "")`
- 必填参数:`location`
- 可选参数:`parent`
- 可选参数:`cancellationToken`
- 可选参数:`packageName`
- 返回值:`UniTask<GameObject>`
### 六、回调式加载
#### `UniTask LoadAssetAsync(string location, int priority, LoadAssetCallbacks loadAssetCallbacks, object userData, string packageName = "")`
#### `UniTask LoadAssetAsync(string location, Type assetType, int priority, LoadAssetCallbacks loadAssetCallbacks, object userData, string packageName = "")`
`LoadAssetCallbacks` 中的回调签名:
- 成功:`(string assetName, object asset, float duration, object userData)`
- 失败:`(string assetName, LoadResourceStatus status, string errorMessage, object userData)`
- 进度:`(string assetName, float progress, object userData)`
适合:
- 旧式回调链
- 想同时观察进度与错误
- 不方便直接 `await` 的场景
### 七、句柄接口
#### `AssetHandle LoadAssetSyncHandle<T>(string location, string packageName = "") where T : UnityEngine.Object`
#### `AssetHandle LoadAssetAsyncHandle<T>(string location, string packageName = "") where T : UnityEngine.Object`
适合:
- 需要直接管理底层句柄
- 需要和 YooAsset 句柄 API 配合
### 八、释放与回收
#### `void UnloadAsset(object asset)`
- 必填参数:`asset`
- 返回值:无
说明:
- 本质上是把资源对象从 `_assetPool``Unspawn`
#### `void UnloadUnusedAssets()`
- 返回值:无
说明:
- 释放资源对象池中未使用的对象
- 并对所有已成功初始化的包执行 `UnloadUnusedAssetsAsync`
#### `void ForceUnloadAllAssets()`
- 返回值:无
说明:
- 强制卸载所有资源
- WebGL 下会直接警告并退出
#### `void ForceUnloadUnusedAssets(bool performGCCollect)`
- 必填参数:`performGCCollect`
- 返回值:无
说明:
- 委托给 `ResourceComponent` 控制实际的系统资源回收时机
## 常见用法
### 1. 初始化默认包
```csharp
using AlicizaX;
using AlicizaX.Resource.Runtime;
using UnityEngine;
public sealed class ResourceInitExample : MonoBehaviour
{
private async void Start()
{
bool ok = await GameApp.Resource.InitPackageAsync();
Debug.Log($"Init default package success: {ok}");
}
}
```
### 2. 初始化远端包
```csharp
using AlicizaX;
using UnityEngine;
public sealed class HostModeInitExample : MonoBehaviour
{
private async void Start()
{
await GameApp.Resource.InitPackageAsync(
packageName: "DefaultPackage",
hostServerURL: "https://cdn.example.com/game",
fallbackHostServerURL: "https://backup.example.com/game");
}
}
```
### 3. 同步加载配置资源
```csharp
using AlicizaX;
using UnityEngine;
public sealed class SyncLoadExample : MonoBehaviour
{
private void Start()
{
TextAsset config = GameApp.Resource.LoadAsset<TextAsset>("Config/GameBalance");
Debug.Log(config != null ? config.text : "Config missing");
}
}
```
### 4. 异步加载图片资源
```csharp
using AlicizaX;
using UnityEngine;
public sealed class AsyncLoadExample : MonoBehaviour
{
private async void Start()
{
Sprite sprite = await GameApp.Resource.LoadAssetAsync<Sprite>("UI/Common/Atlas/Icon_Star");
Debug.Log(sprite != null);
}
}
```
### 5. 加载并实例化 GameObject
```csharp
using AlicizaX;
using UnityEngine;
public sealed class InstantiateExample : MonoBehaviour
{
private GameObject _hero;
private async void Start()
{
_hero = await GameApp.Resource.LoadGameObjectAsync("Character/Hero.prefab", transform);
}
private void OnDestroy()
{
if (_hero != null)
{
Destroy(_hero);
}
}
}
```
说明:
- 这里不需要对 `_hero` 再调用 `UnloadAsset`
- 销毁实例时 `AssetsReference` 会自动释放源资源引用
### 6. 使用回调式加载并监听进度
```csharp
using AlicizaX;
using AlicizaX.Resource.Runtime;
using UnityEngine;
public sealed class CallbackLoadExample : MonoBehaviour
{
private async void Start()
{
var callbacks = new LoadAssetCallbacks(
(assetName, asset, duration, userData) =>
{
Debug.Log($"Load success: {assetName}, duration: {duration:F3}s");
},
(assetName, status, errorMessage, userData) =>
{
Debug.LogError($"Load failed: {assetName}, {status}, {errorMessage}");
},
(assetName, progress, userData) =>
{
Debug.Log($"Progress {assetName}: {progress:P0}");
});
await GameApp.Resource.LoadAssetAsync(
"UI/Common/Icon",
priority: 0,
loadAssetCallbacks: callbacks,
userData: null);
}
}
```
### 7. 下载资源版本并更新清单
```csharp
using AlicizaX;
using UnityEngine;
public sealed class UpdateManifestExample : MonoBehaviour
{
private async void Start()
{
var versionOp = GameApp.Resource.RequestPackageVersionAsync();
await versionOp.ToUniTask();
if (versionOp.Status == YooAsset.EOperationStatus.Succeed)
{
string version = versionOp.PackageVersion;
var manifestOp = GameApp.Resource.UpdatePackageManifestAsync(version);
await manifestOp.ToUniTask();
Debug.Log($"Manifest updated to: {version}");
}
}
}
```
### 8. 创建下载器
```csharp
using AlicizaX;
using UnityEngine;
public sealed class DownloaderExample : MonoBehaviour
{
private async void Start()
{
var downloader = GameApp.Resource.CreateResourceDownloader();
downloader.BeginDownload();
await downloader.ToUniTask();
Debug.Log($"Download status: {downloader.Status}");
}
}
```
## 运行行为细节
### 1. 重复加载同一路径会优先复用缓存
当前实现通过 `_assetPool` 保存 `AssetObject`,同一路径的资源会复用池内对象。
好处:
- 减少重复加载
- 降低频繁生成/销毁底层句柄的开销
### 2. 并发同路径加载会合并请求
如果同一路径资源正在加载中:
- 后续请求不会重复发起底层加载
- 会等待首个请求完成后再复用结果
### 3. `LoadGameObject` 与 `LoadAsset<T>` 的释放语义不同
#### `LoadAsset<T>`
- 返回的是资源对象本身
- 如有需要可以显式 `UnloadAsset(asset)`
#### `LoadGameObject`
- 返回的是实例化后的场景对象
- 源 prefab 资源句柄通过 `AssetsReference` 绑定到实例对象
- 通常直接 `Destroy(instance)` 即可
### 4. 低内存行为
触发 `OnLowMemory()` 时:
- 会调用 `_forceUnloadUnusedAssetsAction`
- 实际清理由 `ResourceComponent` 驱动
### 5. `AssetInfo` 有内部缓存
多次 `GetAssetInfo` 会命中 `_assetInfoMap`
适合:
- 频繁查询资源合法性
- UI 打开前做路径校验
### 6. 失败行为
以下情况通常会导致异常或错误日志:
- 资源路径为空
- 包不存在
- 资源定位无效
- 初始化失败
## 最佳实践
- 统一资源路径规范,避免大小写和目录命名混乱
- 默认优先异步加载
- 把“包初始化 / 版本更新 / 资源下载 / 资源使用”分成不同阶段处理
- UI、音频、图集等高复用资源尽量走缓存复用
- 对需要长期驻留的资源,业务层要明确生命周期,不要频繁反复加载
## 常见错误
### 1. 包未初始化就加载资源
现象:
- 资源查找失败
- 返回空对象或直接异常
规避:
- 确保启动流程中先执行 `InitPackageAsync`
### 2. 默认包名与实际包名不一致
现象:
- 路径正确但查不到资源
规避:
- 统一 `DefaultPackageName`
- 多包场景显式传入 `packageName`
### 3. 手动 `UnloadAsset` 已实例化场景对象
现象:
- 语义不清晰
- 可能导致资源生命周期混乱
规避:
- `LoadGameObject` 返回的实例优先 `Destroy`
### 4. 解密服务类型名错误
现象:
- 初始化时反射失败
规避:
- `DecryptionServices` 必须是可反射创建的完整类型名
## 性能注意事项
- 避免在主线程大量同步加载
- 常用资源优先缓存复用
- 合理设置:
- `AssetAutoReleaseInterval`
- `AssetCapacity`
- `AssetExpireTime`
- `AssetPriority`
- 高并发异步加载时,优先复用路径和包配置,发挥内部合并机制优势