服务定位器
获取任意框架或游戏服务时唯一需要调用的静态类。
获取任意服务时唯一需要调用的类 · 所有框架与游戏服务的全局注册表 · 始终已初始化——无需替换
功能概述
服务定位器是一个小型静态注册表,只回答一个问题:"X 服务在哪里?"启动引导在启动时把 23 个框架服务逐一注册进来。你的代码在任何地方调用 ServiceLocator.Resolve<IMyService>() 就能拿回实例——不需要场景引用、不需要单例、不需要编译期接线。如果想替换某个实现(例如用你自己的存档系统替代默认实现),调用一次 Replace<T>() 即可;其他任何东西都不必改动。
快速示例
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)] // Bootstrap registers services first — class-level attribute
public class GameController : MonoBehaviour
{
[SerializeField] private AudioClip _clickSound;
// Cache in a field; never call Resolve in Update().
private IAudioService _audio;
private ISaveService _save;
void Awake()
{
// Bootstrap has already called ServiceLocator.Register<T>() for each service.
_audio = ServiceLocator.Resolve<IAudioService>();
_save = ServiceLocator.Resolve<ISaveService>();
}
public void OnPlayButtonPressed()
{
_audio.PlaySfx(_clickSound);
}
}
如果想在没有真实存档系统的情况下运行——为了测试,或想关闭该功能——换入内置的无操作实现:
// In a test setup, or anywhere before the code under test resolves it:
ServiceLocator.Replace<ISaveService>(new NullSaveService());
// New Resolve calls now return the replacement.
// Code that cached the old instance earlier keeps using that old instance —
// the locator never updates references you already hold.
完整 API 一览
所有方法都是 ServiceLocator 类上的静态方法。所有方法都仅限主线程(参见行为与边界情况)。
核心操作
-
void Register<T>(T instance)—— 以T为键添加一个新服务。如果T已注册过服务则抛出ServiceAlreadyRegisteredException;如果T是具体类则抛出ArgumentException(键必须是接口或抽象类)。用它注册你自己的游戏服务;框架自身的服务由 Bootstrap 注册。 -
T Resolve<T>()—— 获取已注册的服务。如果T没有注册任何服务则抛出ServiceNotRegisteredException。每次调用都是一次字典查找——不要每帧调用;在Awake/Start中解析一次,把结果缓存到字段里。 -
void Replace<T>(T instance)—— 覆盖或创建一个注册项(已存在也不会报错)。它不会释放被替换掉的实例——它无法知道你是否仍持有引用。如果被换出的服务拥有内部 GameObject(时间、对象池、音频、UI 框架、调度器、补间与补间序列各自会创建一个[CGS] …GameObject),需要你自行释放,否则该 GameObject 会在本次会话余下的时间里一直存活并持续运转:var previous = ServiceLocator.Resolve<ITimeService>() as System.IDisposable; ServiceLocator.Replace<ITimeService>(new NullTimeService()); previous?.Dispose(); // after the swap, so nothing resolves a disposed instance -
bool TryResolve<T>(out T service)—— 软查找;未注册时返回false并把service置为null。不抛异常。用于可选服务。 -
bool IsRegistered<T>()—— 只检查是否存在,不获取实例。 -
void Unregister<T>()—— 移除注册项。未注册时调用也是安全的(调用两次没有问题)。
调试与测试辅助
void Reset()—— 清空所有注册项。仅在 Editor 和 Development 构建中可用;在 Release 构建中被编译剔除,因此无法在生产代码路径中调用。IReadOnlyDictionary<Type, object> GetRegistrySnapshot()—— 所有已注册服务的只读副本。仅限 Editor。int RegisteredServiceCount—— 已注册条目的实时数量。仅限 Editor。
异常
ServiceLocatorException—— 所有服务定位器错误的基类型;捕获它即可处理所有此类错误。ServiceAlreadyRegisteredException——Register因键已存在而失败;暴露.ServiceType属性。ServiceNotRegisteredException——Resolve失败;暴露.ServiceType以及一条列出最常见原因的消息。
缺失服务与可选查找
服务定位器本身没有无操作替代——注册表总是会初始化,在每次进入 Play 以及 Editor 程序集重载之后都是如此。如果某个服务缺失,Resolve<T>() 会立即抛出 ServiceNotRegisteredException。这是有意为之:你会在需要该服务的那一行代码处立刻发现问题,而不是稍后无声地出错。
如果想对可选服务做优雅处理,改用 TryResolve<T>:
if (ServiceLocator.TryResolve<IAudioService>(out var audio))
Debug.Log("Audio service is available");
else
Debug.LogWarning("Audio service not registered");
行为与边界情况
-
缓存你解析到的东西。
Resolve<T>()每次都是一次字典查找。在Update()中调用它每帧都在浪费时间。在Awake或Start中解析一次,把引用保存在字段里。 -
仅限主线程。 在 Debug 构建中,所有公共方法都会断言自己运行在 Unity 主线程上。从
Task.Run、线程池或任何后台线程调用会以"[ServiceLocator] Main thread only."失败。如果后台线程需要某个服务,先在主线程上解析它,再把引用传进去。 -
键只能是接口或抽象类。
Register<ConcreteClass>(impl)会抛出ArgumentException。始终用接口或抽象类作为T。这样每个服务都可以在调用方毫不知情的情况下被替换。 -
不会遍历接口继承。 假如
ISaveService派生自某个IPersistenceService,而你只注册了ISaveService,那么Resolve<IPersistenceService>()会抛出异常。注册表是一个精确类型的字典。要么把两个键都注册到同一个实例上,要么解析你注册时使用的确切键。 -
Replace不会重定向你已经持有的引用。Replace<T>()之后,新的Resolve<T>()调用返回新实例,但缓存了旧实例的代码会继续使用旧实例。请在消费者解析之前替换实现——通常放在更早运行的Awake里,或测试的 setup 中。 -
过早解析。 Bootstrap 在第一个场景加载前注册所有框架服务,因此场景代码总能使用框架服务。但如果你自己的代码在之后才注册额外的服务——例如在某个 MonoBehaviour 的
Awake里——那么在那个时间点之前运行的代码还无法解析它们。ServiceNotRegisteredException的消息会带你排查常见的顺序问题。 -
Domain Reload 与 IL2CPP。 无论 Editor 中是否启用 Domain Reload,注册表都会在每次进入 Play 时清空并重新初始化。如果你用 IL2CPP(Unity 的 AOT 编译后端)构建,通过服务调用传递的自定义数据类型需要在你自己的项目中添加
link.xml条目,以免构建的代码裁剪把它们移除;框架自身的类型已经受到保护。