关于依赖注入框架VContainer DIIOC 的学习记录
文章目录
- 前言
- 一、VContainer核心概念
- 1.DI(Dependency Injection(依赖注入))
- 2.scope(域,作用域)
- 二、练习例子
- 1.Hello,World!
- 步骤一,编写一个底类。HelloWorldService
- 步骤二,编写使用低类的类。GamePresenter
- 步骤三,编写对应的LitetimeScope,GameLifetimeScope。用来注册C#类依赖
- 2.补充,控制反转 (IoC)
- 3.构造函数注入
- 4.方法注入(其他的和上边一样)
- 5.字段、属性注入
- 6.C#complex 类型注册
- 7.接口注册
- 8.多接口注册
- 9.自动注册所有已实现的接口
- 10.注册所有已实现的接口和具体类型
- 11.注册生命周期Maker接口
- 11.注册实例
- 12.将实例注册为接口
- 13.注册泛型
- 14.使用委托注册
- 15.注册工厂
- 16.注册仅需运行时参数的 Func<> 工厂方法
- 17.注册需要容器依赖项和运行时参数的 Func<> 工厂方法
- 18.MonoBehaviour 注册方式
- 19.注册 ScriptableObject 配置数据
- 20.注册集合类型解析
- 21.注册容器回调方法
- 三、VContainer 配合 UniTask 使用
- 1.异步初始化接口
- 2.异步资源加载
前言
稍微记录一下VContainer依赖注入框架的学习过程。这个框架主要是解决代码的依赖问题,假设有个类A和一个类B,类B需要使用到类A作为依赖,这个时候就会有很强的依赖性,当类A改变代码逻辑的时候,会影响到类B,不符合代码原则,所以要进行依赖倒置原则,这个时候我们一般需要准备一个接口A来抽象一下类B需要用到的功能,这样当类A修改的时候不会影响到类B。这里主要是学习经过框架来进行依赖并且进一步提升自己的代码质量
github:https://github.com/hadashiA/VContainer
文章地址:https://vcontainer.hadashikick.jp
一、VContainer核心概念
1.DI(Dependency Injection(依赖注入))
DI(依赖注入)是 OOP 中的一种通用技术,旨在从代码中删除无关的依赖项。 它为您的对象图带来了可测试性、可维护性、可扩展性或任何类型的可交换性。
在所有的编程范式中,基本设计是弱模块耦合和强模块内聚。 如您所知,OOP(面向对象编程)通过对象来实现。
1.Objects 隐藏了其职责 (封装) 的详细信息。
2.对象将 其职责范围之外的工作转移到其他对象。
实际上,从根本上这样做是有问题的。 如果在类代码中编写委托对象,则意味着源代码级别的紧密耦合。 从类中排除无关依赖项的唯一方法是从外部传入它。
因此,如果你的类从外部接收依赖项,它将需要来自外部的帮助。 DI 是一种技术,它有助于一个地方完全在外部解决依赖关系。
延伸阅读:
曼宁 |.NET 中的依赖关系注入
适用于 Unity 的轻量级 IoC 容器 - Seba 的实验室
2.scope(域,作用域)
scope,域,作用域,可以认为是VContainer当中的一个空间范围,在这个范围内的依赖注入是独立的。一般我们会为场景的预制体设置单独的scope,离开这个scope后对应的DI即不在生成。
每一个scope都有**生命周期(LifeTime)**加入这个scope是通过场景或者预制体创建出来的话,那么这个scope的生命周期会在场景或者预制体销毁之后结束,除此之外还有一个RootScope,就是最核心的scope,通常RootScope是注入到单例上面的。
二、练习例子
1.Hello,World!
在场景中,使用LifetimeScope脚本挂载节点的方式来作为容器并且控制范围。
所以所有LifetimeScope子类来进行注册C#代码的依赖项,在启动场景的时候,Lifetime会自动构建Container并且反派到自己的PlayerLoopSystem。
步骤一,编写一个底类。HelloWorldService
public class HelloWorldService{public void Hello(){DLogger.Log("Hello World");}}
步骤二,编写使用低类的类。GamePresenter
public class GamePresenter : ITickable{readonly HelloWorldService helloWorldService;public GamePresenter(HelloWorldService helloWorldService){this.helloWorldService = helloWorldService;}public void Tick(){helloWorldService.Hello();}}
这里基础ITickable接口,这个接口对应Unity程序中的Uodate,继承实现Marker接口接会自动将这个类型注入到对应GameTimeScope的PlayerLoopSystem里面。
步骤三,编写对应的LitetimeScope,GameLifetimeScope。用来注册C#类依赖
public class GameLifetimeScope : LifetimeScope
{protected override void Configure(IContainerBuilder builder){builder.Register<HelloWorldService>(Lifetime.Singleton); builder.RegisterEntryPoint<GamePresenter>();}
}
这里通过重写LifeimeScope的Configure方法来进行C#依赖项。HelloWorldService是作为单例进行注册。
RegisterEntryPoint() 是用于注册与 Unity 的 PlayerLoop 事件相关的接口的别名。
类似于 Register(Lifetime.Singleton)。作为()
在不依赖 MonoBehaviour 的情况下注册生命周期事件有助于领域逻辑和表示的解耦!
由于LiteTimeScope自身依赖Mono,所以创建节点挂载脚本的方式来进行控制。
启动脚本后打印如下:
2.补充,控制反转 (IoC)
IoC (Inversion of Control):将具有控制流责任的对象作为入口点。在简单和传统的编程中,入口点是负责中断用户输入的地方。
创建一个HelloUI脚本用于保存对应的脚本对象。
public class HelloUI : MonoBehaviour
{ #region 自动生成public Text m_textStart;public Button m_btnHello;public void Start(){RefRoot refRoot = GetComponent<RefRoot>();m_textStart = refRoot.GetText(0);m_btnHello = refRoot.GetButton(1);}#endregion
}
在正常的 Unity 编程中,您可以在 HelloUI 中嵌入逻辑调用,但如果使用的是 DI,则可以将 HelloUI 和任何控制流分开。
namespace MyGame
{public class GamePresenter : IStartable{readonly HelloWorldService helloWorldService;readonly HelloScreen helloScreen; public GamePresenter(HelloWorldService helloWorldService,HelloScreen helloScreen){this.helloWorldService = helloWorldService;this.helloScreen = helloScreen;}void IStartable.Start(){helloScreen.m_btnHello.onClick.AddListener(() => helloWorldService.Hello());}}
}
通过这样做,我们成功地分离了域逻辑 / 控制流 / 视图组件。
GamePresenter 的演示者:仅负责 Control Flow。
HelloWorldService 中:只对可以随时随地调用的功能负责
HelloUI:仅负责 View。
在 VContainer 中,您需要注册依赖的 MonoBehaviour。不要忘记注册 HelloUI。
3.构造函数注入
构造函数里,只需要写一个需要依赖注入的函数,成员变量里就可以随时获得对象。如下例子ClassB构造函数的参数是ClassA,我们的classA变量就可以随时使用
class ClassB : IStartable,ITickable
{readonly ClassA a;public ClassB(ClassA a){Debug.Log("ClassA构造函数注入");this.a = a;}public void Start(){a.Start();}public void Tick(){a.Update();}
}
class ClassA
{public ClassA(){Debug.Log("ClassA构造");}public void Start(){Debug.Log("Start");}public void Update() {Debug.Log("Update");}
}
public class GameLifetimeScope : LifetimeScope
{//public UIView helloScreen;protected override void Configure(IContainerBuilder builder){builder.RegisterEntryPoint<ClassB>();builder.Register<ClassA>(Lifetime.Singleton);}
}
4.方法注入(其他的和上边一样)
class ClassB : IStartable,ITickable
{private ClassA a;[Inject]public void GetClassA(ClassA a) {Debug.Log("方法注入");this.a = a;}public void Start(){a.Start();}public void Tick(){a.Update();}
}
5.字段、属性注入
class ClassB : IStartable,ITickable
{[Inject]private ClassA a;public void Start(){a.Start();}public void Tick(){a.Update();}
}
6.C#complex 类型注册
class ServiceA : IServiceA, IInputPort, IDisposable { /* ... */ }
注册具体类型如下
builder.Register<ServiceA>(Lifetime.Singleton);
它可以像这样解析:
class ClassA
{public ClassA(ServiceA serviceA) { /* ... */ }
}
7.接口注册
builder.Register<IServiceA, ServiceA>();
它可以像这样解析:
class ClassA
{public ClassA(IServiceA serviceA) { /* ... */ }
}
8.多接口注册
builder.Register<ServiceA>(Lifetime.Singleton).As<IServiceA, IInputPort>();
它可以像这样解析:
class ClassA
{public ClassA(IServiceA serviceA) { /* ... */ }
}class ClassB
{public ClassB(IInputPort inputPort) { /* ... */ }
}
9.自动注册所有已实现的接口
builder.Register<ServiceA>(Lifetime.Singleton).AsImplementedInterfaces();
它可以像这样解析:
class ClassA
{public ClassA(IServiceA serviceA) { /* ... */ }
}class ClassB
{public ClassB(IInputPort inputPort) { /* ... */ }
}
10.注册所有已实现的接口和具体类型
builder.Register<ServiceA>(Lifetime.Singleton).AsImplementedInterfaces().AsSelf();
它可以像这样解析:
class ClassA
{public ClassA(IServiceA serviceA) { /* ... */ }
}class ClassB
{public ClassB(IInputPort inputPort) { /* ... */ }
}class ClassC
{public ClassC(ServiceA serviceA) { /* ... */ }
}
11.注册生命周期Maker接口
class GameController : IStartable, ITickable, IDisposable { /* ... */ }
builder.RegisterEntryPoint<GameController>();关键的区别在于它是否在 PlayerLoopSystem 中运行。Register<GameController>(Lifetime.Singleton).AsImplementedInterfaces()
如果要自定义入口点的异常处理,可以使用以下内容注册回调。```c
builder.RegisterEntryPointExceptionHandler(ex =>
{UnityEngine.Debug.LogException(ex);// Additional process ...
});
如果您有多个 EntryPoints,则可以选择使用以下声明作为分组。
builder.UseEntryPoints(entryPoints =>
{entryPoints.Add<ScopedEntryPointA>();entryPoints.Add<ScopedEntryPointB>();entryPoints.Add<ScopedEntryPointC>().AsSelf();entryPoints.OnException(ex => ...)
});
这与以下相同:
builder.RegisterEntryPoint<ScopedEntryPointA>();
builder.RegisterEntryPoint<ScopedEntryPointB>();
builder.RegisterEntryPoint<ScopedEntryPointC>().AsSelf();
builder.RegisterEntryPointExceptionHandler(ex => ...);
11.注册实例
// ...
var obj = new ServiceA();
// ...builder.RegisterInstance(obj);
RegisterIntance总是有一个生命周期,所以它没有参数。
它可以像这样解析:
class ClassA
{public ClassA(ServiceA serviceA) { /* ... */ }
}
向 RegisterInstance 注册的实例不由容器管理。
Dispose 不会自动执行。
方法注入不会自动执行
如果您希望容器管理创建的实例,请考虑改用以下内容
register(_ => 实例, …)
RegisterComponent(…)
12.将实例注册为接口
builder.RegisterInstance<IInputPort>(serviceA);builder.RegisterInstance(serviceA).As<IServiceA, IInputPort>();builder.RegisterInstance(serviceA).AsImplementedInterfaces();
寄存器特定于类型的参数
如果类型不是唯一的,但您有要在启动时注入的依赖项,则可以使用以下内容:
builder.Register<SomeService>(Lifetime.Singleton).WithParameter<string>("http://example.com");
或者,您可以使用键命名 paramter。
builder.Register<SomeService>(Lifetime.Singleton).WithParameter("url", "http://example.com");
它可以像这样解析:
class SomeService
{public SomeService(string url) { /* ... */ }
}
此 Register 仅在注入 时起作用。SomeService
class OtherClass
{// ! Errorpublic OtherClass(string hogehoge) { /* ... */ }
}
13.注册泛型
class GenericType<T>
{// ...
}
builder.Register(typeof(GenericType<>), Lifetime.Singleton);
它可以像这样解析:
class SomeService
{public SomeService(GenericType<int> closedGenericType) { /* ... */ }
}
14.使用委托注册
实例创建可以委托给 lambda 表达式或其他方法或类。
builder.Register<IFoo>(_ =>
{var foo = new Foo();// Do something;return foo;
}, Lifetime.Scoped);
它可以像这样解析:
class ClassA
{public ClassA(IFoo foo) { /* ...*/ }
}
表达式中可使用的第一个参数是IObjectResolver。通过它,我们可以检索并使用已注册的对象。
builder.Register<IFoo>(container =>
{var serviceA = container.Resolve<ServiceA>();return serviceA.ProvideFoo();
}, Lifetime.Scoped);
IObjectResolver.Instantiate还可用于生成执行 inject 的游戏对象。
builder.Register(container =>
{return container.Instantiate(prefab);
}, Lifetime.Scoped);
15.注册工厂
VContainer 通常会在首次解析时构造已注册的依赖项(已注册的实例除外)。若需更精确地控制依赖项的创建时机,可通过注册并使用工厂函数来实现。
工厂函数是 Func<> 委托,其解析方式与其他依赖项相同。它们可用于随时创建一个或多个其他依赖项。
注意
尽管名为"工厂",但工厂函数既可以返回新创建的对象,也可以返回现有对象。这在需要为相同类型的依赖项映射不同键时非常实用,例如为本地多人游戏中每个玩家分配专属的控制器服务。
以下示例中,依赖项解析仅会发生一次。虽然可以在工厂函数中显式调用 IObjectResolver API,但在此特定 Create() 方法内不会触发依赖解析(这正是构造函数的作用所在)。
class FooFactory
{public FooFactory(DependencyA dependencyA){this.dependencyA = dependencyA;}public Foo Create(int b) => new Foo(b, dependencyA);
}
builder.Register<FooFactory>(Lifetime.Singleton); // Registered// ...var factory = container.Resolve<FooFactory>(); // Dependency resolution occurs// ...var foo1 = factory.Create(1); // No resolution needed here
var foo2 = factory.Create(2); // No resolution needed here
var foo3 = factory.Create(3); // No resolution needed here
虽然如上所述创建工厂类很有用,但简单的工厂也可以通过 lambda 表达式来注册。
(补充说明:这种 lambda 表达式的方式更简洁,适合逻辑简单的工厂场景,避免了单独创建工厂类的开销。)
注意
VContainer 不会自动管理工厂返回对象的生命周期。若工厂返回的是实现了 IDisposable 的对象,需手动处理其释放。这种情况下,使用工厂类是更理想的选择——因为工厂类本身由 VContainer 管理,只要实现 IDisposable 接口,VContainer 就会自动清理其资源。
(补充说明:对于需要资源管理的场景,建议将工厂逻辑封装为类而非 lambda,以利用 VContainer 的生命周期管理能力。)
16.注册仅需运行时参数的 Func<> 工厂方法
如果你的工厂不需要其他依赖项,你可以像这样注册它:
builder.RegisterFactory<int, Foo>(x => new Foo(x));
以下是使用它的方法:
class ClassA
{readonly Func<int, Foo> factory;public ClassA(Func<int, Foo> factory){this.factory = factory;}public void DoSomething(){var foo = factory(100);// ...}
}
17.注册需要容器依赖项和运行时参数的 Func<> 工厂方法
若工厂方法需要其他依赖项,需通过注册接受 IObjectResolver 并返回目标 Func<> 的方式实现:
builder.RegisterFactory<int, Foo>(container => // container 是 IObjectResolver
{var dependency = container.Resolve<Dependency>(); // 按作用域解析return x => new Foo(x, dependency); // 每次工厂调用时执行
}, Lifetime.Scoped);
此版本需要指定 Lifetime 参数,用于控制内部 Func<> 的生成频率(即外部 Func<> 的调用频率)。
带依赖项的工厂解析方式完全相同,如下所示:
class ClassA
{readonly Func<int, Foo> factory;public ClassA(Func<int, Foo> factory) => this.factory = factory;public void DoSomething(){var foo = factory.Invoke(100);// ...}
}
工厂内部可配合使用 IObjectResolver 的扩展方法:
builder.RegisterFactory<CharacterType, CharacterActor>(container =>
{return characterType =>{var characterPrefab = ...return container.Instantiate(characterPrefab, parentTransform);}
}, Lifetime.Scoped);
工厂方法注册
工厂可注册为能转换为 Func<> 的任何委托(包括普通方法)。无论底层实现多复杂,最终都能以 Func<> 形式使用。
假设存在以下工厂类:
class FooFactory
{public Foo Create(int b) => new Foo(b, dependencyA);// ...
}
无需了解完整类即可将其作为 Func<> 使用:
builder.Register<FooFactory>(Lifetime.Singleton);
builder.RegisterFactory(container => container.Resolve<FooFactory>().Create, Lifetime.Singleton);// 使用示例
var factory = container.Resolve<Func<int, Foo>>();
var foo1 = factory(1); // 仍可获取原工厂实例(若实现 IDisposable 将随容器释放)
var originalFactory = container.Resolve<FooFactory>();
18.MonoBehaviour 注册方式
基本注册方法
通过 LifetimeScope 的 [SerializeField] 注册
[SerializeField]
YourBehaviour yourBehaviour;// ...
builder.RegisterComponent(yourBehaviour);
注意
RegisterComponent 与 RegisterInstance 类似,区别在于使用 RegisterComponent 注册的 MonoBehaviour 即使未被显式 Resolve 也会进行依赖注入。
从场景中注册
builder.RegisterComponentInHierarchy<YourBehaviour>();
通过预制体实例化注册
builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped, "NewGameObjectName");
以接口形式注册组件
builder.RegisterComponentInHierarchy<YourBehaviour>().AsImplementedInterfaces();
指定父级 Transform
// 在指定 Transform 下新建 GameObject
builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped).UnderTransform(parent);// 在指定 Transform 下实例化预制体
builder.RegisterComponentInNewPrefab(prefab, Lifetime.Scoped).UnderTransform(parent);// 在指定 Transform 下查找已有组件
builder.RegisterComponentInHierarchy<YourBehaviour>().UnderTransform(parent);
或运行时动态查找:
builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped).UnderTransform(() => {// ...return parent;});
注册为 DontDestroyOnLoad
// 新建 DontDestroyOnLoad 的 GameObject
builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped).DontDestroyOnLoad();// 实例化 DontDestroyOnLoad 的预制体
builder.RegisterComponentInNewPrefab(prefab, Lifetime.Scoped).DontDestroyOnLoad();
批量注册 MonoBehaviour
builder.UseComponents(components =>
{components.AddInstance(yourBehaviour);components.AddInHierarchy<YourBehaviour>();components.AddInNewPrefab(prefab, Lifetime.Scoped);components.AddOnNewGameObject<YourBehaviour>(Lifetime.Scoped, "name");
});
等价于:
builder.RegisterComponent(yourBehaviour);
builder.RegisterComponentInHierarchy<YourBehaviour>();
builder.RegisterComponentInNewPrefab(prefab, Lifetime.Scoped);
builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped, "name");
19.注册 ScriptableObject 配置数据
在 Unity 中使用 VContainer 注册 ScriptableObject 配置数据的完整指南:
基础配置类定义
首先定义可序列化的配置数据结构:
[Serializable]
public class CameraSettings
{public float MoveSpeed = 10f;public float DefaultDistance = 5f;public float ZoomMax = 20f;public float ZoomMin = 5f;
}[Serializable]
public class ActorSettings
{public float MoveSpeed = 0.5f;public float FlyingTime = 2f;public Vector3 FlyingInitialVelocity = Vector3.zero;
}
创建 ScriptableObject 资源
创建包含所有游戏设置的 ScriptableObject 主资源:
[CreateAssetMenu(fileName = "GameSettings", menuName = "MyGame/Settings")]
public class GameSettings : ScriptableObject
{[SerializeField]public CameraSettings cameraSettings; // 注意修正了变量名大小写一致性[SerializeField] public ActorSettings actorSettings;
}
通过 Unity 菜单创建资源:
右键点击 Project 窗口
选择 Create → MyGame → Settings
命名为 “GameSettings” 并配置各参数
在 LifetimeScope 中注册
将创建的 ScriptableObject 资源注册到依赖注入容器:
public class SomeLifetimeScope : LifetimeScope
{[SerializeField]GameSettings settings; // 拖拽分配在 Inspector 中创建的 GameSettings 资源protected override void Configure(IContainerBuilder builder){// 注册各个配置部分builder.RegisterInstance(settings.cameraSettings);builder.RegisterInstance(settings.actorSettings);// 也可以注册整个 GameSettings 对象builder.RegisterInstance(settings);}
}
使用注册的配置
在需要的地方通过构造函数注入使用配置:
public class CameraController
{private readonly CameraSettings _settings;public CameraController(CameraSettings settings){_settings = settings;}public void Update(){// 使用配置参数float speed = _settings.MoveSpeed;// ...}
}
最佳实践建议
资源管理
I.将 GameSettings 资源放在 Resources 文件夹外
II.通过明确的路径或地址ables系统加载
生命周期
I.使用 RegisterInstance 注册的 ScriptableObject 是单例
II.配置数据通常使用 Singleton 生命周期
模块化设计
I.为不同的系统分离配置类
II.避免一个庞大的全局配置类
III.运行时修改
IV.如需运行时修改配置,考虑:
builder.RegisterInstance(settings.cameraSettings).AsImplementedInterfaces();
测试支持
I.可以为测试创建专门的测试配置资源
II.通过不同的 LifetimeScope 注册不同的配置
这种模式非常适合管理游戏中的各种参数配置,特别是需要设计师频繁调整的数值参数。
20.注册集合类型解析
VContainer 支持通过集合接口自动解析特定类型,用于处理多实现的依赖关系。
基本用法
注册多个实现同一接口的类型后,可通过集合接口一次性解析:
// 注册多个IDisposable实现
builder.Register<IDisposable, A>(Lifetime.Scoped);
builder.Register<IDisposable, B>(Lifetime.Scoped);
解析方式
支持两种集合接口形式注入:
- IEnumerable 形式
class ServiceConsumer
{public ServiceConsumer(IEnumerable<IDisposable> disposables) {// 可遍历所有IDisposable实现foreach (var disposable in disposables){// ...}}
}
- IReadOnlyList 形式
class ServiceConsumer
{public ServiceConsumer(IReadOnlyList<IDisposable> disposables){// 可通过索引访问var first = disposables[0];// ...}
}
内部机制说明
此功能主要由框架内部使用(如 ITickable 等标记接口),但开发者也可利用此模式处理多实现场景。
典型应用场景
插件式架构
不同模块实现相同接口,主系统需要收集所有实现
事件处理器
多个事件监听器需要统一管理
策略模式
多种策略实现统一接口,根据上下文选择使用
生命周期注意事项
集合中的元素生命周期保持独立,不受集合接口解析影响。即:
1.如果元素注册为Scoped,其生命周期与所属Scope绑定
2.如果元素注册为Singleton,则保持单例
扩展建议
可通过自定义集合类型实现更精确的控制:
// 注册为自定义集合类型
builder.RegisterInstance(new DisposableCollection(disposables));// 自定义集合类
public class DisposableCollection : IReadOnlyList<IDisposable>
{// 实现接口成员...
}
21.注册容器回调方法
容器构建回调
您可以在容器构建时注册任意回调操作:
builder.RegisterBuildCallback(container =>
{// 容器构建完成后执行的逻辑var serviceA = container.Resolve<ServiceA>();var serviceB = container.Resolve<ServiceB>();// ...
});
特性说明:
回调参数使用 IObjectResolver 接口(详情参见容器API文档)
适合执行需要容器完全构建后才能进行的初始化操作
在容器所有注册完成但尚未开始解析前触发
容器销毁回调
注册容器销毁时执行的回调:
builder.RegisterDisposeCallback(container =>
{// 容器销毁时执行的清理逻辑// 如:释放非托管资源、保存状态等
});
典型应用场景:
资源初始化/清理
builder.RegisterBuildCallback(resolver => {resolver.Resolve<AssetLoader>().Preload();
});builder.RegisterDisposeCallback(resolver => {resolver.Resolve<AssetLoader>().ReleaseAll();
});
服务热连接
builder.RegisterBuildCallback(resolver => {resolver.Resolve<NetworkService>().Connect();
});
数据持久化
builder.RegisterDisposeCallback(resolver => {resolver.Resolve<GameState>().Save();
});
生命周期注意事项:
构建回调按注册顺序执行
销毁回调按注册的逆序执行(类似栈结构)
回调中抛出的异常会中断后续回调执行
高级用法:
// 组合使用构建和销毁回调
builder.RegisterBuildCallback(container => {var logger = container.Resolve<ILogger>();var timer = new Stopwatch();timer.Start();// 在销毁时记录容器存活时间builder.RegisterDisposeCallback(_ => {timer.Stop();logger.Log($"Container lifetime: {timer.ElapsedMilliseconds}ms");});
});
三、VContainer 配合 UniTask 使用
UniTask 是为 Unity 优化的高性能 async/await 解决方案,与 VContainer 深度集成后可实现更强大的异步编程能力。
当项目中安装 com.cysharp.unitask 包后,VCONTAINER_UNITASK_INTEGRATION 编译符号会自动启用,激活以下功能:
1.异步初始化接口
IAsyncStartable 接口
public class FooController : IAsyncStartable
{public async UniTask StartAsync(CancellationToken cancellation){await LoadSomethingAsync(cancellation); // 异步加载资源await InitializeSubSystems(); // 初始化子系统// ...}
}
注册方式:
builder.RegisterEntryPoint<FooController>();
与同步版 IStartable 类似,注册后 StartAsync 方法会自动执行。
执行时序控制
PlayerLoop 阶段定制
await UniTask.Yield(PlayerLoopTiming.FixedUpdate); // 指定在FixedUpdate阶段继续执行
注意:
所有 StartAsync 方法默认在主线程同步执行
不同 PlayerLoop 阶段不会等待异步操作完成
异常处理
完善的错误处理机制
try
{await DangerousOperationAsync();
}
catch (Exception ex)
{// 完全支持try/catch语法
}
全局异常监听:
UniTaskScheduler.UnobservedTaskException += ex =>
{Debug.LogError($"未捕获的异步异常: {ex}");
};
作用域专属处理器:
builder.RegisterEntryPointExceptionHandler(ex =>
{Debug.LogError($"作用域内异常: {ex}");
});
取消Token
public async UniTask StartAsync(CancellationToken cancellation)
{await LongRunningTask(cancellation); // 当LifetimeScope销毁时自动取消
}
2.异步资源加载
与生命周期作用域配合
var extraAsset = await Addressables.LoadAssetAsync<ExtraAsset>(key);using (LifetimeScope.EnqueueParent(parentScope))
using (LifetimeScope.Enqueue(builder => builder.RegisterInstance(extraAsset))
{await SceneManager.LoadSceneAsync("AdditiveScene");
}
最佳实践建议
1.初始化顺序控制
[Inject] IInitializable[] _initializables; // 依赖注入所有需要同步初始化的服务
2.混合使用同步/异步初始化
public class HybridService : IStartable, IAsyncStartable
{public void Start() { /* 同步初始化 */ }public async UniTask StartAsync(CancellationToken cancellation) { /* 异步初始化 */ }
}
3.资源加载模式
// 推荐在LifetimeScope构建阶段预加载关键资源
builder.RegisterBuildCallback(async resolver =>
{var asset = await resolver.Resolve<IAssetLoader>().LoadAsync();
});
这套集成方案特别适合需要复杂异步初始化流程的项目,如:
网络游戏连接流程
大型场景的渐进式加载
需要严格资源管理的应用