[关闭]
@qidiandasheng 2021-01-25T17:54:55.000000Z 字数 7730 阅读 3175

控制反转(IOC)和依赖注入(DI)(😁)

架构


控制反转(IOC)和依赖注入(DI)

介绍

控制反转(Inversion of Control,缩写为IOC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。通过控制反转,对象在被创建的时候,由一个外部容器,将其所依赖的对象的引用传递给它。控制反转并不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。

依赖注入(Dependency Injection,简称DI),它是一种具体的编码技巧。也可以说,依赖注入是实现控制反转的一种方式,也就是说是控制反转这种设计原则的一种实现。用一句话来概括就是:不通过new()的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递 (或注入)给类使用。

IOC和DI由什么关系呢?

其实它们是同一个概念的不同角度描述。由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:依赖注入,相对IoC而言,依赖注入明确描述了“被注入对象依赖IoC容器配置依赖对象”。

依赖注入的例子

Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式的new一个B的对象。但这样会存在一些问题:

  1. @interface ClassA : NSObject
  2. @property(nonatomic, strong) ClassB *b;
  3. @end
  4. @implementation ClassA
  5. -(instancetype)init {
  6. self = [super init];
  7. if (self) {
  8. self.b = [[ClassB alloc] initWithName:@"dasheng"];
  9. }
  10. return self;
  11. }
  12. -(void)dosomething{
  13. NSLog(@"I am %@.", self.b.name);
  14. }

下面修改最大的点是我们将 B 作为 A 的构造函数的一部分传入,在调用 A 的构造方法之前就已经初始化好的 B。像这种非自身主动创建依赖,而是通过外部传入的方式,我们就称为依赖注入。

  1. @interface ClassA : NSObject
  2. @property (strong, nonatomic) ClassB* b;
  3. -(instancetype)initWithBObject:(ClassB*)b;
  4. -(void)dosomething;
  5. @end
  6. @implementation ClassA
  7. -(instancetype)initWithBObject:(ClassB *)bj {
  8. self = [super init];
  9. if (self) {
  10. self.b = b;
  11. }
  12. return self;
  13. }
  14. -(void)dosomething{
  15. NSLog(@"I am %@.", self.b.name);
  16. }
  17. @end
  18. ====================
  19. //这里我们调用方就是IOC容器
  20. ClassB *objB = [[ClassB alloc] initWithName:@"dasheng"];
  21. ClassA *objA = [[ClassA alloc] initWithBObject:objB];
  22. [objA dosomething];

使用场景

所依赖模块拥有多种实现

相关协议及实现协议的多种数据库

  1. @protocol DBInterface <NSObject>
  2. - (User *)queryUserWithId:(NSString *)userId;
  3. @end
  4. @interface MongoDB : NSObject<DBInterface>
  5. @end
  6. @interface SQLite : NSObject<DBInterface>
  7. @end

内部直接创建依赖的数据库,内部硬编码为使用mongodb

  1. @interface UserSystem()
  2. @property(nonatomic, strong)id<DBInterface> db;
  3. @end
  4. @implementation UserSystem
  5. - (instancetype)init{
  6. self = [super init];
  7. if (self) {
  8. self.db = MongoDB.new;
  9. }
  10. return self;
  11. }
  12. - (User *)getUserWithId:(NSString *)userId{
  13. return [self.db queryUserWithId:userId];
  14. }
  15. @end

由外部注入所依赖的数据库,外部可以随时更改实现,实现了开闭原则,不必修改UserSystem内部实现

  1. @interface UserSystem()
  2. @property(nonatomic, strong)id<DBInterface> db;
  3. @end
  4. @implementation UserSystem
  5. - (instancetype)initWithDB:(id<DBInterface>)db{
  6. self = [super init];
  7. if (self) {
  8. self.db = db;
  9. }
  10. return self;
  11. }
  12. - (User *)getUserWithId:(NSString *)userId{
  13. return [self.db queryUserWithId:userId];
  14. }
  15. @end
  16. ======================
  17. //外部注入时可随时切换数据库
  18. MongoDB *mongoDB = MongoDB.new;
  19. SQLite *sqlite = SQLite.new;
  20. UserSystem *userSystem = [[UserSystem alloc] initWithDB:mongoDB];
  21. User *user = [userSystem getUserWithId:@"111"];

创建依赖对象复杂

创建对象的代码充斥在应用里的各个角落,如果类的构造函数有变动,那么需要修改用到该类的各个地方。使用依赖注入框架的话,统一在DI框架里进行对象的创建,也就是做到了创建和使用分离。

下图就是UserSystem对象直接依赖了实现DBInterface接口的实现MongoDB在内部进行创建。

1.png-9kB

下图则把Assembly当做DI注入器,由它来创建MongoDB对象注入到UserSystem,而UserSystem只要依赖于接口DBInterface获取到注入对象使用接口。做到了和实现类的解耦。

2.png-12.3kB

哪些场景不适合引入DI?

不同于编程风格和设计哲学,软件设计模式的优缺点和适用性是有普遍共识的。 其中DI也不是万能的,只能解决一类特定的问题。 过度设计与缺乏设计一样罪恶,不要沉溺于任何一种自己熟悉的设计模式。

利用依赖注入(DI)实现注解

Java注解介绍

注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK 1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

主要是Java里的一种语法。Java注解可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java虚拟机可以保留标注内容,在运行时可以获取到标注内容。 当然它也支持自定义Java注解。

在Java中的注解其实就是接口,简单来说就是java通过动态代理的方式为你生成了一个实现了"接口"的实例(对于当前的实体来说,例如类、方法、属性域等,这个代理对象是单例的),然后对该代理实例的属性赋值,这样就可以在程序运行时(如果将注解设置为运行时可见的话)通过反射获取到注解的配置信息。

注解的作用

反射介绍

对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为反射机制。

iOS实现注解

Java的annotation没有行为,只能有数据,实际上就是一组键值对而已。通过解析(parse)Class文件就能把一个annotation需要的键值对都找出来。

于是问题就变成:

这里通过一个示例来说明,代码在github上DSUnits:

  1. //车辆model,根据注解设置的单位来输出最后的值
  2. @interface DSCarModel : NSObject<IOCComponents>
  3. DSUnits(price_t1, PRICE_UNIT_F)
  4. @property(nonatomic, copy)NSString *price_t1;
  5. DSUnits(price_t2, PRICE_UNIT_Y)
  6. @property(nonatomic, copy)NSString *price_t2;
  7. DSUnits(price_t3, PRICE_UNIT_WY)
  8. @property(nonatomic, copy)NSString *price_t3;
  9. @end
  1. /// 扫描所有遵守IOCComponents协议的类(即需要实现注解的类)存放入全局数组中
  2. - (void)scanClasses {
  3. int classCount = objc_getClassList(NULL, 0);
  4. Class *classList = (Class *)malloc(classCount * sizeof(Class));
  5. classCount = objc_getClassList(classList, classCount);
  6. // 用于存放需要IoC容器处理的类的OC数组
  7. NSMutableArray *temp = @[].mutableCopy;
  8. // 获得IOCComponents协议,用于判断标记
  9. Protocol *protocol = objc_getProtocol("IOCComponents");
  10. for (int i = 0; i < classCount; i++) {
  11. Class clazz = classList[i];
  12. NSString *className = NSStringFromClass(clazz);
  13. //检查IOCComponents标记,有标记的类才被IoC容器处理
  14. if (class_conformsToProtocol(clazz, protocol)) {
  15. [temp addObject:className];
  16. }
  17. }
  18. // 将IoC需要处理的类存储起来
  19. self.DIClasses = temp;
  20. // 由于类列表是malloc创建的,需要手动释放
  21. free(classList);
  22. // 根据注解属性处理依赖注入
  23. [self scanAnnotation];
  24. }
  25. /// 遍历属性,根据运行时得到的属性对应的类型存入类对应的全局实例对象的dsUnitsAnnotationMapper中
  26. - (void)scanAnnotation {
  27. // 对scanClasses中得到的需要IoC容器处理的类进行遍历
  28. for (NSUInteger i = 0; i < self.DIClasses.count; i++) {
  29. NSString *className = self.DIClasses[i];
  30. Class class = NSClassFromString(className);
  31. unsigned int outCount;
  32. // 反射出所有属性
  33. objc_property_t *props = class_copyPropertyList(class, &outCount);
  34. // 保存所有注解属性,注解属性包含了位置索引(index)、名称(name)和类型(type),通过一个模型类SGAnnotation来存储
  35. NSMutableArray *annotations = @[].mutableCopy;
  36. // 保存所有的属性信息,每个属性包含了名称(name)和类型(type),通过一个模型类SGProperty来存储
  37. NSMutableArray *properties = @[].mutableCopy;
  38. // 遍历所有属性
  39. for (NSUInteger i = 0; i < outCount; i++) {
  40. objc_property_t prop = props[i];
  41. NSString *propName = [[NSString alloc] initWithCString:property_getName(prop) encoding:NSUTF8StringEncoding];
  42. // 这一段代码用于从描述属性的字符串中获取到类型,用到了正则和字串处理
  43. NSString *propAttrs = [[NSString alloc] initWithCString:property_getAttributes(prop) encoding:NSUTF8StringEncoding];
  44. NSRange range = [propAttrs rangeOfString:@"@\".*\"" options:NSRegularExpressionSearch];
  45. if (range.location != NSNotFound) {
  46. range.location += 2;
  47. range.length -= 3;
  48. NSString *typeName = [propAttrs substringWithRange:range];
  49. // 如果当前属性为注解属性,则记录进annotaions
  50. if ([self isPropertyAnnotationByType:typeName]) {
  51. DSAnnotation *anno = [DSAnnotation new];
  52. anno.index = i;
  53. anno.name = propName;
  54. anno.type = typeName;
  55. [annotations addObject:anno];
  56. }
  57. // 记录每一条属性
  58. DSProperty *sp = [DSProperty new];
  59. sp.name = propName;
  60. sp.type = typeName;
  61. [properties addObject:sp];
  62. }
  63. } // scan class properties end
  64. // 从容器中得到类的实例
  65. id diInstance = [self getInstanceByClassName:className];
  66. NSMutableDictionary *mutableDic = [NSMutableDictionary dictionary];
  67. // 遍历注解,得到所有被修饰的属性
  68. for (NSUInteger i = 0; i < annotations.count; i++){
  69. DSAnnotation *annotation = annotations[i];
  70. DSProperty *prop = properties[annotation.index + 1];
  71. [mutableDic setObject:annotation.type forKey:prop.name];
  72. }
  73. [diInstance setValue:mutableDic forKey:@"dsUnitsAnnotationMapper"];
  74. } // scan classes end
  75. }

依赖注入的三方库

objection和typhoon

介绍

区别

源码

Demo

依赖注入框架使用的Demo

EXTConcreteProtocol

EXTConcreteProtocol浅析,这个库主要就是通过协议来向Class注入对应的函数,但是需要遍历遵守协议的所有Class,从而实现动态注入。

一次高效的依赖注入,这个方式基本逻辑差不多,但这里不是通过遍历的方式,比如工程里有上万个类,那遍历的效率就会很低。所以这里的方案是在Class调用对应的函数找不到时进入消息转发,然后再实现动态注入:

1、开始我们在 +load 方法中做准备工作,把所有的协议都存到链表中。

2、在 __attribute__((constructor)) 中做是否能执行注入的检查。

3、现在我们 hook NSObject 的 +resolveInstanceMethod:+resolveClassMethod:

4、在 hook 中进行检查,如果该类有遵守了我们实现了注入的协议,那么就给该类注入容器中的方法。

参考

反射、注解与依赖注入总结
iOS控制反转(IoC)与依赖注入(DI)的实现
java注解是怎么实现的?
Android编译时注解框架系列1-什么是编译时注解

关于iOS依赖注入那些事
iOS 依赖注入:Objection 和 Typhoon

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注