AlicizaX/Client/Assets/Books/Framework/Runtime/Resource.md
2026-04-01 13:20:06 +08:00

16 KiB
Raw Blame History

Resource

模块概述

Resource 模块基于 YooAsset 封装了资源包初始化、版本管理、资源查询、同步/异步加载、实例化、缓存回收与低内存处理能力。

它是多个模块的底层依赖:

  • UI:加载窗口和 Widget 预制体
  • Audio:加载音频资源
  • Localization:加载语言表
  • GameObjectPool:加载池对象原始 prefab
  • Scene:主场景与附加场景的资源加载链路

从当前实现来看,ResourceService 还额外提供了:

  • 多包管理
  • 同一路径并发加载合并
  • 资源对象池缓存
  • 低内存时统一回收入口
  • 下载、版本、清单更新能力

快速开始

最少步骤

  1. 在场景中挂载 ResourceComponent
  2. 配置 PlayMode、默认包名和解密服务名
  3. 游戏启动时调用 InitPackageAsync
  4. 通过 GameApp.ResourceAppServices.Require<IResourceService>() 加载资源

最小示例

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);
    }
}

架构说明

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:内置资源 + 缓存 + 远端下载
  • WebPlayModeWeb 环境资源拉取方式

并发加载合并机制

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

说明:

  • 会使用当前 DownloadingMaxNumFailedTryAgain

三、资源查询

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
  • 返回值:无

说明:

  • 本质上是把资源对象从 _assetPoolUnspawn

void UnloadUnusedAssets()

  • 返回值:无

说明:

  • 释放资源对象池中未使用的对象
  • 并对所有已成功初始化的包执行 UnloadUnusedAssetsAsync

void ForceUnloadAllAssets()

  • 返回值:无

说明:

  • 强制卸载所有资源
  • WebGL 下会直接警告并退出

void ForceUnloadUnusedAssets(bool performGCCollect)

  • 必填参数:performGCCollect
  • 返回值:无

说明:

  • 委托给 ResourceComponent 控制实际的系统资源回收时机

常见用法

1. 初始化默认包

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. 初始化远端包

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. 同步加载配置资源

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. 异步加载图片资源

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

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. 使用回调式加载并监听进度

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. 下载资源版本并更新清单

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. 创建下载器

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. LoadGameObjectLoadAsset<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
  • 高并发异步加载时,优先复用路径和包配置,发挥内部合并机制优势