本文目的 向大家介绍:
在开发中为何要使用IoC
如何开实现一个精简的IoC
使用IoC前后代码带来怎样的变化
我当前在开发的IoC类库
如果你对1、2、3都已经很熟了,并且对我的项目感兴趣,可以直接跳我的IoC仓库.完整的工程地址在https://github.com/kakashiio/Unity-IOC ,该IoC仓库也是我的Unity游戏框架计划https://github.com/kakashiio/Unity-SourceFramework 中的一部分.
为什么要使用IoC 想象一下,当你在实现一个UI管理器UIManager时,当在UIManager中需要加载UI资源时,你是通过何种方式加载资源的.
一般开发诸如AssetManager、TimeManager、EventManager等管理器(Manager)时.喜欢采用静态方法或单例.这样做是为了使得项目能方便地引用这些管理器.
常见的实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class UIManager { public void Create <T >(Action<T> onCreate ) where T : IUI { string assetPath = _GetAssetPath<T>(); AssetManager.Instantiate<GameObject>((go)=>{ var t = new T(); t.Init(go); onCreate?.Invoke(t); }); } }
或
1 2 3 4 5 6 7 8 9 10 11 12 public class UIManager { public void Create <T >(Action<T> onCreate ) where T : IUI { string assetPath = _GetAssetPath<T>(); Singleton<AssetManager>.Instance.Instantiate<GameObject>((go)=>{ var t = new T(); t.Init(go); onCreate?.Invoke(t); }); } }
虽然静态方法或单例都能实现想要的效果,但或多或少会带来负面的效果.比如耦合严重,难以测试等等.因此本文引入一种已经很成熟的设计思路IoC,一步步实现一个简单的IoC容器,并且将IoC应用到实际中.大家也可以对比感受引入IoC前后代码发生的变化.
IoC简述 IoC(Inversion of Control,控制反转)通常也被称为DI(Dependency Injection,依赖注入).他是将传统对象依赖从内部指定改为外部决定的过程.比如上面的UIManager中内部指定了使用AssetManager.当使用IoC设计时,代码会修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class UIManager { private IAssetManager _AssetManager; public UIManager (IAssetManager assetManager ) { _AssetManager = assetManager; } public void Create <T >(Action<T> onCreate ) where T : IUI { string assetPath = _GetAssetPath<T>(); _AssetManager.Instantiate<GameObject>((go)=>{ var t = new T(); t.Init(go); onCreate?.Invoke(t); }); } }
这是引入IoC最简单的例子,即把内部采用哪个IAssetManager实现的权力转移给外部,因此称为IoC(Inversion of Control,控制反转),由于UIManager依赖了IAssetManager而且将其实现通过外部构造传入,因此也称DI(Dependency Injection,依赖注入).
但是这样的代码明显不够方便,因为需要自己在构造时传入IAssetManager,如果只是UIManager需要传入IAssetManager实例还好,实际上可以预见的是SceneManager、UnitManager、EffectManager等类可能都需要IAssetManager,那么最终可能会有类似这样的代码:
1 2 3 4 5 6 7 8 9 10 11 12 public class Main { public void Init () { var assetManager = new AssetManager(); var uiManager = new UIManager(assetManager); var sceneManager = new SceneManager(assetManager); var unitManager = new UnitManager(assetManager); var effectManager = new EffectManager(assetManager); } }
这样的代码重复、而且没有意义、不同的人反复在这里添加自己的代码也容易引发冲突和错误.我们应该编写一个更智能的IoC框架来帮助我们完成这些事情.
编写IoC框架 添加依赖 由于我们需要大量使用反射完成一些工作,因此通过PackageManager依赖我之前开源的用于反射的Packagehttps://github.com/kakashiio/Unity-Reflection
打开Unity的PackageManager并点击左上角的“+”按钮,选择"Add package from git URL..."并填入该地址https://github.com/kakashiio/Unity-Reflection.git#1.0.0
IoC容器 定义IoC容器接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public interface IIOCContainer { object InstanceAndInject (Type type ) ; T InstanceAndInject <T >() ; void Inject (object obj, bool recursive = false ) ; object FindObjectOfType (Type type ) ; T FindObjectOfType <T >() where T : class ; List<object > FindObjectsOfType (Type type ) ; List <T > FindObjectsOfType <T >() where T : class ; }
IIOCContainer接口主要定义了一个IOC容器对外提供的服务.比如外部可以通过FindObjectOfType查找某个类型在容器中创建的实例、或者通过InstanceAndInject创建一个指定类型的对象,InstanceAndInject方法与new创建对象不同在于InstanceAndInject创建的对象会被容器管理,同时会自动按设计的约定注入字段.
这里每个方法都写了比较详细的注释.如果目前大家还不是很清楚,主要可能是对于IoC还不太熟悉,这关系不大.后面会通过实际使用的例子回过来深入介绍细节.接下来先把该接口的实现和另外几个比较重要的类的源码给出来,目前大家只要先大概浏览一下即可.
实现IoC容器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 public class IOCContainer : IIOCContainer { private ITypeContainer _TypeContainer; private List<object > _Instances = new List<object >(); private HashSet<object > _InjectedObj = new HashSet<object >(); private Dictionary<Type, object > _FindCache = new Dictionary<Type, object >(); public IOCContainer (ITypeContainer typeContainer ) { _TypeContainer = typeContainer; var inheritedFromIOCComponent = Reflections.GetTypes(_TypeContainer, typeof (IOCComponent)); var typesWithIOCComponent = Reflections.GetTypesWithAttributes(_TypeContainer, inheritedFromIOCComponent); foreach (var type in typesWithIOCComponent) { _Instances.Add(_Instance(type)); } foreach (var instance in _Instances) { Inject(instance); } } public object InstanceAndInject (Type type ) { var instance = _Instance(type); Inject(instance); return instance; } public T InstanceAndInject <T >() { return (T) InstanceAndInject(typeof (T)); } public void Inject (object obj, bool recursive = false ) { if (obj == null ) { return ; } if (obj.GetType().IsPrimitive) { return ; } if (recursive) { if (_InjectedObj.Contains(obj)) { return ; } _InjectedObj.Add(obj); } var propertiesOrFields = Reflections.GetPropertiesAndFields<Autowired>(obj); foreach (var propertyOrField in propertiesOrFields) { var fieldValue = FindObjectOfType(propertyOrField.GetFieldOrPropertyType()); propertyOrField.SetValue(obj, fieldValue); if (recursive) { Inject(fieldValue, true ); } } } public object FindObjectOfType (Type type ) { if (_FindCache.ContainsKey(type)) { return _FindCache[type]; } foreach (object instance in _Instances) { if (type.IsAssignableFrom(instance.GetType())) { _FindCache.Add(type, instance); return instance; } } return null ; } public T FindObjectOfType <T >() where T : class { return FindObjectOfType(typeof (T)) as T; } public List<object > FindObjectsOfType (Type type ) { return _FindObjectsOfType(typeof (object ), o => o); } public List <T > FindObjectsOfType <T >() where T : class { return _FindObjectsOfType(typeof (T), o => o as T); } private object _Instance(Type type) { return Activator.CreateInstance(type); } private List <T > _FindObjectsOfType <T >(Type type, Func<object , T> mapper ) where T : class { List<T> list = new List<T>(); foreach (object instance in _Instances) { var objType = instance.GetType(); if (type.IsAssignableFrom(objType)) { list.Add(mapper(instance)); } } return list; } }
上面的实现中有几个类尚未定义,下面继续定义缺失的类.
IoC容器需要的其他类定义 1 2 3 4 5 6 [AttributeUsage(AttributeTargets.Class) ] public class IOCComponent : Attribute { }
1 2 3 4 5 6 7 [AttributeUsage(AttributeTargets.Field|AttributeTargets.Property) ] public class Autowired : Attribute { }
OK,依然如前所述,对于接触不多的人而言,该框架信息量确实比较大,请先放松.接下来通过实际使用的例子,再深入讲解上面的源码.
IoC框架使用示例 定义各种测试用Manager 日志管理类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 [IOCComponent ] public class LogManager { private LogLevel _LogLevel = LogLevel.Debug; public void Log (LogLevel level, string templte, params object [] args ) { if (level < _LogLevel) { return ; } string msg = args == null || args.Length == 0 ? templte : string .Format(templte, args); msg = $"[{level} ] Frame={Time.frameCount} Time={Time.time} -- {msg} " ; switch (level) { case LogLevel.Debug: case LogLevel.Info: Debug.Log(msg); break ; case LogLevel.Warning: Debug.LogWarning(msg); break ; case LogLevel.Exception: Debug.LogException(new Exception(msg)); break ; case LogLevel.Error: Debug.LogError(msg); break ; } } } public enum LogLevel{ Debug, Info, Warning, Exception, Error }
该类只是用于做简单的日志记录,会被后续其他Manager依赖使用.
注意到这个管理类上使用IOCComponent这一Attribute进行修饰.后续其他管理类也是如此.后续会解释为什么要这么做.
协程管理类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [IOCComponent ] public class CoroutineManager { private CoroutineRunner _CoroutineRunner; public CoroutineManager () { var go = new GameObject("CoroutineRunner" ); _CoroutineRunner = go.AddComponent<CoroutineRunner>(); GameObject.DontDestroyOnLoad(go); } public void StartCoroutine (IEnumerator enumerator ) { _CoroutineRunner.StartCoroutine(enumerator); } } public class CoroutineRunner : MonoBehaviour { }
该类只是用于简单的协程调用,会被后续其他Manager依赖使用
资源管理类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [IOCComponent ] public class AssetManager { [Autowired ] private CoroutineManager _CoroutineManager; [Autowired ] private LogManager _LogManager; public void LoadAsync <T >(string assetPath, Action<T> onLoaded ) where T : Object { _CoroutineManager.StartCoroutine(_LoadAsync(assetPath, onLoaded)); } private IEnumerator _LoadAsync<T>(string assetPath, Action<T> onLoaded) where T : Object { _LogManager.Log(LogLevel.Debug, "Loading {0}" , assetPath); yield return new WaitForSeconds (3 ) ; T loadedAsset = default (T); _LogManager.Log(LogLevel.Debug, "Loaded {0} asset={1}" , assetPath, loadedAsset); onLoaded?.Invoke(loadedAsset); } }
资源管理类,可以看到该类依赖了CoroutineManager和LogManager,但是没有对外提供这两个对象的设置.
GameObject管理类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [IOCComponent ] public class GameObjectManager { [Autowired ] private AssetManager _AssetManager; [Autowired ] private LogManager _LogManager; public void Instantiate (string assetPath, Action<GameObject> onLoaded ) { _AssetManager.LoadAsync(assetPath, (GameObject prefab) => { if (prefab == null ) { _LogManager.Log(LogLevel.Debug, "Failed to instantiate {0}" , assetPath); return ; } var go = GameObject.Instantiate(prefab); onLoaded?.Invoke(go); }); } }
GameObject管理类,可以看到该类也依赖了CoroutineManager和LogManager,和AssetManager一样没有对外提供这两个对象的设置.
那么,这样的代码是否能工作呢,我们接着编写测试类.
测试依赖注入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class BasicDemo : MonoBehaviour { private void Awake () { var typeContainer = new TypeContainerCollection(new [] { new TypeContainer(Assembly.GetExecutingAssembly()), new TypeContainer(typeof (IOCComponent).Assembly) }); var iocContainer = new IOCContainer(typeContainer); GameObjectManager gameObjectManager = iocContainer.FindObjectOfType<GameObjectManager>(); gameObjectManager.Instantiate("" , null ); } }
可以看到,这个类主要就是创建了一个IoC容器IOCContainer对象,接着从该容器中查找GameObjectManager,接着通过GameObjectManager实例化一个对象.
可以把该类挂到场景中任意对象上,然后运行场景.发现Unity会输出以下Log.
可以看到,我们并没有手动为各个Manager传入依赖,但是目前而言,通过IOCContainer为我们自动创建的Manager确实自动注入了依赖.
为何能实现注入 那么是什么时候创建了各个管理器的实例,又是什么时候设置了管理器之间的依赖.我们重新对IOCContainer的构造函数进行分析.
IOCContainer的构造函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public IOCContainer (ITypeContainer typeContainer ){ _TypeContainer = typeContainer; var inheritedFromIOCComponent = Reflections.GetTypes(_TypeContainer, typeof (IOCComponent)); var typesWithIOCComponent = Reflections.GetTypesWithAttributes(_TypeContainer, inheritedFromIOCComponent); foreach (var type in typesWithIOCComponent) { _Instances.Add(_Instance(type)); } foreach (var instance in _Instances) { Inject(instance); } }
注释1的代码表示从_TypeContainer中获取从IOCComponent这一Attribute继承的所有Attribute,如果_TypeContainer中包含了IOCComponent,那么返回的列表中也会有IOCComponent.
_TypeContainer为ITypeContainer类型,顾名思义,它是类型容器,用于返回我们可能需要处理的所有类型.具体使用我会在Unity-Reflection库中补充文档说明.
注释2的代码表示从_TypeContainer中获取类型列表,该列表中的类型需要满足:类上使用了inheritedFromIOCComponent列表中任意Attribute进行修饰.其实按我们目前的例子看,由于我们的所有Manager都使用了IOCComponent进行修饰,那么这里的列表如果仅包含IOCComponent,应当也能查询到我们定义的管理类.那么为什么不直接使用new List<Type> { typeof(IOCComponent) }替代注释1返回的inheritedFromIOCComponent呢.这是因为我想增加一点拓展性.当你想让自己定义的Attribute也能被IOCContainer识别时,你的Attribute可以从IOCComponent继承,那么注释1将能找到你自己定义的Attribute,此时你用自己定义的Attribute修饰类时,该类也能被查找到.
注释3的循环作用为遍历注释2返回的类型列表,并且调用_Instance方法将其实例化,并添加到_Instances列表中,以便后续有其他查找需求.目前_Instance方法只是简单通过Activator.CreateInstance(type);创建了实例并返回.
注释4的循环作用为遍历注释3实例化的_Instances列表,并调用Inject方法进行字段的依赖注入.我们的各个Manager字段的注入就是在此方法中进行的.接下来详细讲解Inject方法
Inject方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public void Inject (object obj, bool recursive = false ){ if (obj == null ) { return ; } if (obj.GetType().IsPrimitive) { return ; } if (recursive) { if (_InjectedObj.Contains(obj)) { return ; } _InjectedObj.Add(obj); } var propertiesOrFields = Reflections.GetPropertiesAndFields<Autowired>(obj); foreach (var propertyOrField in propertiesOrFields) { var fieldValue = FindObjectOfType(propertyOrField.GetFieldOrPropertyType()); propertyOrField.SetValue(obj, fieldValue); if (recursive) { Inject(fieldValue, true ); } } }
该方法主要用于对字段进行依赖注入.
注释1主要用于当需要递归注入时,如果发现一个对象已经注入过,则跳过,防止递归陷入死循环.
注释2获取obj中所有使用Autowired这一Attribute修饰的字段或属性.Autowired为前面定义的Attribute,我们通过这一Attribute标识哪些字段需要容器自动注入.
注释3通过FindObjectOfType从IoC容器中找到类型和字段或属性类型相匹配的对象,查找会匹配类型.我们后面再细讲FindObjectOfType是如何实现的.
注释4将注释3找到的对象设置进字段,完成该字段注入.
注释5如果开启递归注入,则对该字段的值也进行注入.
FindObjectOfType方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public object FindObjectOfType (Type type ){ if (_FindCache.ContainsKey(type)) { return _FindCache[type]; } foreach (object instance in _Instances) { if (type.IsAssignableFrom(instance.GetType())) { _FindCache.Add(type, instance); return instance; } } return null ; }
注释1-Start到注释1-End中间的代码为从_FindCache中进行查找.如果之前已经通过该方法查到过该类型,那么该类型会进入_FindCache缓存,后续查找的时间复杂度就仅为O(1).
注释2-Start到注释2-End中间的代码为从已经实例化的_Instances中查找有没有能赋值给type类型的对象,如果有,则加入到_FindCache缓存并且返回结果.可以发现这里使用了Type.IsAssignableFrom进行类型匹配,因此如果你的字段使用了接口或某个父类,也能正常进行注入.接下来我们增加一个ILogManager接口测试一下.
将LogManager改为接口 新增接口ILogManager 1 2 3 4 5 6 7 8 9 10 11 12 13 public interface ILogManager { public void Log (LogLevel level, string templte, params object [] args ) ; } public enum LogLevel{ Debug, Info, Warning, Exception, Error }
新增ILogManager接口,并将LogManager中的枚举LogLevel删移动过来.
修改LogManager 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 [IOCComponent ] public class LogManager : ILogManager { private LogLevel _LogLevel = LogLevel.Debug; public void Log (LogLevel level, string templte, params object [] args ) { if (level < _LogLevel) { return ; } string msg = args == null || args.Length == 0 ? templte : string .Format(templte, args); msg = $"[{level} ] Frame={Time.frameCount} Time={Time.time} -- {msg} " ; switch (level) { case LogLevel.Debug: case LogLevel.Info: Debug.Log(msg); break ; case LogLevel.Warning: Debug.LogWarning(msg); break ; case LogLevel.Exception: Debug.LogException(new Exception(msg)); break ; case LogLevel.Error: Debug.LogError(msg); break ; } } }
让LogManager实现ILogManager接口
修改AssetManager的字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 [IOCComponent ] public class AssetManager { [Autowired ] private CoroutineManager _CoroutineManager; [Autowired ] private ILogManager _LogManager; public void LoadAsync <T >(string assetPath, Action<T> onLoaded ) where T : Object { _CoroutineManager.StartCoroutine(_LoadAsync(assetPath, onLoaded)); } private IEnumerator _LoadAsync<T>(string assetPath, Action<T> onLoaded) where T : Object { _LogManager.Log(LogLevel.Debug, "Loading {0}" , assetPath); yield return new WaitForSeconds (3 ) ; T loadedAsset = default (T); _LogManager.Log(LogLevel.Debug, "Loaded {0} asset={1}" , assetPath, loadedAsset); onLoaded?.Invoke(loadedAsset); } }
注释1可以看到之前字段_LogManager从LogManager类型修改为接口类型ILogManager.
同样地,将GameObjectManager中字段_LogManager从LogManager类型修改为接口类型ILogManager.
重新运行场景,发现结果和之前不使用接口是一样的.
如果你想指定所有需要管理的类怎么实现 只需要去掉类定义上面的[IOCComponent],同时在构建IOCContainer时通过配置指定即可.
假设我们有如下的类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class You : IInstanceLifeCycle { [Autowired ] private Word _Word; [Autowired ] [Qualifier(WORD_SPECIAL_INSTANCE) ] private Word _Word2; public void Say () { Debug.LogError($"Say {_Word.GetMsg()} " ); Debug.LogError($"Say {_Word2.GetMsg()} " ); } public void BeforePropertiesOrFieldsSet () { } public void AfterPropertiesOrFieldsSet () { } public void AfterAllInstanceInit () { Say(); } } class Word { private string _Msg = "Hello" ; public string GetMsg () { return _Msg; } }
可以看到You中依赖了两个Word类型的实例.有一个Word通过Qualifier指定了具体实例.
接下来看如何构造IOCContainer.
通过配置构造IOCContainer 1 2 3 4 5 6 7 8 9 10 11 12 public class SpecifyByHand : MonoBehaviour { public const string WORD_SPECIAL_INSTANCE = nameof (WORD_SPECIAL_INSTANCE); void Start () { IOCContainerConfiguration config = new IOCContainerConfiguration() .AddConfigInstanceInfo<You>() .AddConfigInstanceInfo<Word>() .AddConfigInstanceInfo<Word>(WORD_SPECIAL_INSTANCE, new ValueSetter("_Msg" , "Message" )); new IOCContainerBuilder().SetConfiguration(config).Build(); } }
可以看到配置指定了创建两个Word实现和一个You,其中一个Word实例的Qualifier和上面You中字段上的Qualifier一致.
运行会输出:
结束 以上为了更容易讲明白IoC的实现原理,一步步实现了一个极简的IoC容器,实际上该容器还缺少很多特性,比如AOP、比如支持通过配置指定注入不同实例等.更完整的IoC框架已经在下面GITHUB中开发维护.
完整的Package工程地址在https://github.com/kakashiio/Unity-IOC
使用 大家也可以通过PackageManager引用:打开Unity的PackageManager并点击左上角的“+”按钮,选择"Add package from git URL...",加入如下两个地址
致谢 感谢百忙之中阅读本文,如果觉得我的文章帮到了你,欢迎:转载、关注git、为仓库增加star等.你的简单回馈将是我继续创作的动力.