资源提供器

基于 Addressables 的异步资源加载,按 key 引用计数,并以作用域为单位批量清理。

一行代码完成异步资源加载,自动共享资源,清理机制杜绝泄漏。

接口IAssetProvider
关闭开关NullAssetProvider
程序集CommonGameSystem.Assets(可选——需要 Addressables 包)
启动启动时自动注册——无需任何设置

它做什么

资源提供器把 Unity 的 Addressables 系统封装在一个简单的、带引用计数的接口之后。通过地址字符串加载资源,所有使用同一 key 的调用方共享同一份已加载资源——引用计数记录有多少调用方持有它,Release(key) 只有在最后一个调用方释放后才真正卸载。这样一来,"这张贴图该由谁卸载?"就从一个协调难题变成了框架替你完成的记账工作。

你还可以把多次加载归入一个作用域(scope):用 CreateScope() 创建作用域,通过它加载,释放该作用域时会一次性释放它加载过的所有资源。作用域天然适合表达"这个界面用到的全部资源"。

这里没有内存预算,也没有自动淘汰——只有加载、引用计数和按作用域清理。你持有什么,内存里就留着什么。

该服务位于可选的 CommonGameSystem.Assets 程序集中。如果你从项目中移除 Addressables 包,这个程序集会把自己排除在编译之外,框架的其余部分照常编译。

快速上手

Awake 中解析并缓存服务:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)] // Run after the framework has started.
public class MyGameManager : MonoBehaviour
{
    private IAssetProvider _assets;

    private void Awake()
    {
        _assets = ServiceLocator.Resolve<IAssetProvider>();
    }

    private async void OnEnable()
    {
        var prefab = await _assets.LoadAsync<GameObject>("assets/my-prefab");
        if (prefab != null)
            Instantiate(prefab);
    }
}

也可以使用简写别名:IAssetProvider assets = SL.Resolve<IAssetProvider>();

API 参考

加载 / 释放(引用计数)

  • Task<T> LoadAsync<T>(string key, CancellationToken ct = default) — 以类型 T 加载指定地址处的资源(GameObjectScriptableObjectSpriteTextAssetAudioClip 等)。再次加载同一 key 会返回同一份资源并使其引用计数加一。返回的 Task<T> 永不为 null。运行时失败——key 不存在或加载失败——返回 default(T) 并记录一条警告;绝不抛出异常。
  • void Release(string key) — 将该 key 的引用计数减一。计数归零时资源卸载。如果计数已经为零,该调用只记录一条警告,不做任何事;绝不抛出异常。

预加载 / 查询

  • Task PreloadAsync<T>(string key, CancellationToken ct = default) — 加载资源并保留在缓存中,但不返回它。它与 LoadAsync<T> 共用同一套引用计数,因此要与之后的一次 Release(key) 配对。适合在当前场景仍在运行时预热下一个场景的资源。
  • bool IsLoaded(string key) — 当该 key 的引用计数大于零时返回 true。绝不抛异常,也不写日志。
  • int LoadedCount { get; } — 当前持有的不同 key 的数量。这是一个泄漏信号:它统计的是 key 的个数,而不是所有引用计数之和。

作用域

  • IAssetScope CreateScope() — 创建一个作用域。通过作用域加载后,Dispose() 会释放该作用域加载过的一切——对每一次记录在案的加载各执行一次 Release(key)
  • IAssetScope — 提供同样的 LoadAsync<T>PreloadAsync<T>IsLoadedLoadedCount 成员,外加用于批量清理的 Dispose()

示例

加载一个场景的预制件,工作完成后一并清理:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class ScenePopulator : MonoBehaviour
{
    private IAssetProvider _assets;

    private async void Start()
    {
        _assets = ServiceLocator.Resolve<IAssetProvider>();
        using var scope = _assets.CreateScope();

        var player = await scope.LoadAsync<GameObject>("assets/player");
        var ui = await scope.LoadAsync<GameObject>("assets/ui-root");

        if (player != null) Instantiate(player, transform);
        if (ui != null) Instantiate(ui);

        // When the scope is disposed, it releases both
        // "assets/player" and "assets/ui-root".
    }
}

共享同一 key 时引用计数的行为:

var go1 = await _assets.LoadAsync<GameObject>("assets/coin");  // reference count 1
var go2 = await _assets.LoadAsync<GameObject>("assets/coin");  // reference count 2, SAME asset
_assets.Release("assets/coin");  // reference count 1
_assets.Release("assets/coin");  // reference count 0 — the asset unloads

关闭它

ServiceLocator.Replace<IAssetProvider>(new NullAssetProvider());

这会静默所有加载。LoadAsync<T>() 返回一个已完成、结果为 default(T) 的任务,IsLoaded 恒为 falseLoadedCount 恒为 0。完全不会触碰 Addressables。适用于测试,或彻底关闭资源相关功能。它在构造时记录一条警告——而不是每次调用都记录——让你知道它处于启用状态。

常见陷阱

  • 引用计数是唯一的卸载触发器。 没有内存预算,没有最近最少使用(LRU)淘汰,也没有卸载定时器。加载后从不释放,资源就一直留在内存里。盯住 LoadedCount 以发现泄漏。
  • LoadAsync<T> 是编译期泛型方法。 对于 IL2CPP 构建,你加载的资源类型(GameObjectScriptableObjectSpriteTextAssetAudioClip)必须在项目的 link.xml 中保留。包内 README 的 IL2CPP 一节给出了确切条目。
  • 作用域会兜住仍在进行中的加载。 如果作用域在其某次加载仍在进行时被释放,该加载会正常完成,然后立即被释放。不会有任何资源成为孤儿。
  • 类型不匹配是安全的。 如果某个 key 最初以 LoadAsync<GameObject>("key") 加载,之后又调用 LoadAsync<Sprite>("key"),第二次调用返回 null 并记录一条警告——不抛异常。引用计数属于 key 名称,而不属于类型。
  • null 或空 key 被视为你代码中的 bug。 LoadAsync(null) 抛出 ArgumentNullException,全空白的 key 抛出 ArgumentException。这些是编程错误,不同于上面那些绝不抛异常的运行时数据故障。
  • 仅限主线程。 所有公开成员——包括 ReleaseIsLoadedCreateScope 以及作用域的 Dispose——都必须在主线程上运行。在工作线程中,请用调度器Post() 把调用送回主线程执行。

相关页面