资源提供器
基于 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加载指定地址处的资源(GameObject、ScriptableObject、Sprite、TextAsset、AudioClip等)。再次加载同一 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>、IsLoaded与LoadedCount成员,外加用于批量清理的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 恒为 false,LoadedCount 恒为 0。完全不会触碰 Addressables。适用于测试,或彻底关闭资源相关功能。它在构造时记录一条警告——而不是每次调用都记录——让你知道它处于启用状态。
常见陷阱
- 引用计数是唯一的卸载触发器。 没有内存预算,没有最近最少使用(LRU)淘汰,也没有卸载定时器。加载后从不释放,资源就一直留在内存里。盯住
LoadedCount以发现泄漏。 LoadAsync<T>是编译期泛型方法。 对于 IL2CPP 构建,你加载的资源类型(GameObject、ScriptableObject、Sprite、TextAsset、AudioClip)必须在项目的link.xml中保留。包内 README 的 IL2CPP 一节给出了确切条目。- 作用域会兜住仍在进行中的加载。 如果作用域在其某次加载仍在进行时被释放,该加载会正常完成,然后立即被释放。不会有任何资源成为孤儿。
- 类型不匹配是安全的。 如果某个 key 最初以
LoadAsync<GameObject>("key")加载,之后又调用LoadAsync<Sprite>("key"),第二次调用返回 null 并记录一条警告——不抛异常。引用计数属于 key 名称,而不属于类型。 - null 或空 key 被视为你代码中的 bug。
LoadAsync(null)抛出ArgumentNullException,全空白的 key 抛出ArgumentException。这些是编程错误,不同于上面那些绝不抛异常的运行时数据故障。 - 仅限主线程。 所有公开成员——包括
Release、IsLoaded、CreateScope以及作用域的Dispose——都必须在主线程上运行。在工作线程中,请用调度器的Post()把调用送回主线程执行。
相关页面
- 服务定位器 — 解析服务
- 调度器 — 定时器与在主线程上运行代码
- Addressable 场景 — 从 Addressables 叠加加载场景