@qidiandasheng
2021-01-25T09:54:55.000000Z
字数 7730
阅读 3577
架构
控制反转(Inversion of Control,缩写为IOC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。通过控制反转,对象在被创建的时候,由一个外部容器,将其所依赖的对象的引用传递给它。控制反转并不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。
依赖注入(Dependency Injection,简称DI),它是一种具体的编码技巧。也可以说,依赖注入是实现控制反转的一种方式,也就是说是控制反转这种设计原则的一种实现。用一句话来概括就是:不通过new()的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递 (或注入)给类使用。
IOC和DI由什么关系呢?
其实它们是同一个概念的不同角度描述。由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物
Martin Fowler又给出了一个新的名字:依赖注入,相对IoC而言,依赖注入明确描述了“被注入对象依赖IoC容器配置依赖对象”。
Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式的new一个B的对象。但这样会存在一些问题:
-initWithName:age:,那么我们需要修改 A 类中的代码;
@interface ClassA : NSObject@property(nonatomic, strong) ClassB *b;@end@implementation ClassA-(instancetype)init {self = [super init];if (self) {self.b = [[ClassB alloc] initWithName:@"dasheng"];}return self;}-(void)dosomething{NSLog(@"I am %@.", self.b.name);}
下面修改最大的点是我们将 B 作为 A 的构造函数的一部分传入,在调用 A 的构造方法之前就已经初始化好的 B。像这种非自身主动创建依赖,而是通过外部传入的方式,我们就称为依赖注入。
@interface ClassA : NSObject@property (strong, nonatomic) ClassB* b;-(instancetype)initWithBObject:(ClassB*)b;-(void)dosomething;@end@implementation ClassA-(instancetype)initWithBObject:(ClassB *)bj {self = [super init];if (self) {self.b = b;}return self;}-(void)dosomething{NSLog(@"I am %@.", self.b.name);}@end====================//这里我们调用方就是IOC容器ClassB *objB = [[ClassB alloc] initWithName:@"dasheng"];ClassA *objA = [[ClassA alloc] initWithBObject:objB];[objA dosomething];
相关协议及实现协议的多种数据库
@protocol DBInterface <NSObject>- (User *)queryUserWithId:(NSString *)userId;@end@interface MongoDB : NSObject<DBInterface>@end@interface SQLite : NSObject<DBInterface>@end
内部直接创建依赖的数据库,内部硬编码为使用mongodb
@interface UserSystem()@property(nonatomic, strong)id<DBInterface> db;@end@implementation UserSystem- (instancetype)init{self = [super init];if (self) {self.db = MongoDB.new;}return self;}- (User *)getUserWithId:(NSString *)userId{return [self.db queryUserWithId:userId];}@end
由外部注入所依赖的数据库,外部可以随时更改实现,实现了开闭原则,不必修改UserSystem内部实现
@interface UserSystem()@property(nonatomic, strong)id<DBInterface> db;@end@implementation UserSystem- (instancetype)initWithDB:(id<DBInterface>)db{self = [super init];if (self) {self.db = db;}return self;}- (User *)getUserWithId:(NSString *)userId{return [self.db queryUserWithId:userId];}@end======================//外部注入时可随时切换数据库MongoDB *mongoDB = MongoDB.new;SQLite *sqlite = SQLite.new;UserSystem *userSystem = [[UserSystem alloc] initWithDB:mongoDB];User *user = [userSystem getUserWithId:@"111"];
创建对象的代码充斥在应用里的各个角落,如果类的构造函数有变动,那么需要修改用到该类的各个地方。使用依赖注入框架的话,统一在DI框架里进行对象的创建,也就是做到了创建和使用分离。
下图就是UserSystem对象直接依赖了实现DBInterface接口的实现MongoDB在内部进行创建。

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

不同于编程风格和设计哲学,软件设计模式的优缺点和适用性是有普遍共识的。 其中DI也不是万能的,只能解决一类特定的问题。 过度设计与缺乏设计一样罪恶,不要沉溺于任何一种自己熟悉的设计模式。
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK 1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
主要是Java里的一种语法。Java注解可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java虚拟机可以保留标注内容,在运行时可以获取到标注内容。 当然它也支持自定义Java注解。
在Java中的注解其实就是接口,简单来说就是java通过动态代理的方式为你生成了一个实现了"接口"的实例(对于当前的实体来说,例如类、方法、属性域等,这个代理对象是单例的),然后对该代理实例的属性赋值,这样就可以在程序运行时(如果将注解设置为运行时可见的话)通过反射获取到注解的配置信息。
标记作用,用于告诉编译器一些信息让编译器能够实现基本的编译检查。
在Java里有@Override、@Deprecated等。而在iOS里类似的有deprecated()宏定义来实现。
编译时动态处理,动态生成代码
运行时动态处理,获得注解信息
对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为反射机制。
Java的annotation没有行为,只能有数据,实际上就是一组键值对而已。通过解析(parse)Class文件就能把一个annotation需要的键值对都找出来。
于是问题就变成:
接口跟数据的结合
有一个接口
有一组键值对,它里面的数组能支持前面那个接口的功能
怎样才能把这个接口和这个map结合起来呢?
注解是个接口,那么在运行期间获取的那些实例是什么?
在iOS里如何把注解和注解对象(类、字段、方法、局部变量、方法参数等)对应起来?
这里通过一个示例来说明,代码在github上DSUnits:
//车辆model,根据注解设置的单位来输出最后的值@interface DSCarModel : NSObject<IOCComponents>DSUnits(price_t1, PRICE_UNIT_F)@property(nonatomic, copy)NSString *price_t1;DSUnits(price_t2, PRICE_UNIT_Y)@property(nonatomic, copy)NSString *price_t2;DSUnits(price_t3, PRICE_UNIT_WY)@property(nonatomic, copy)NSString *price_t3;@end
/// 扫描所有遵守IOCComponents协议的类(即需要实现注解的类)存放入全局数组中- (void)scanClasses {int classCount = objc_getClassList(NULL, 0);Class *classList = (Class *)malloc(classCount * sizeof(Class));classCount = objc_getClassList(classList, classCount);// 用于存放需要IoC容器处理的类的OC数组NSMutableArray *temp = @[].mutableCopy;// 获得IOCComponents协议,用于判断标记Protocol *protocol = objc_getProtocol("IOCComponents");for (int i = 0; i < classCount; i++) {Class clazz = classList[i];NSString *className = NSStringFromClass(clazz);//检查IOCComponents标记,有标记的类才被IoC容器处理if (class_conformsToProtocol(clazz, protocol)) {[temp addObject:className];}}// 将IoC需要处理的类存储起来self.DIClasses = temp;// 由于类列表是malloc创建的,需要手动释放free(classList);// 根据注解属性处理依赖注入[self scanAnnotation];}/// 遍历属性,根据运行时得到的属性对应的类型存入类对应的全局实例对象的dsUnitsAnnotationMapper中- (void)scanAnnotation {// 对scanClasses中得到的需要IoC容器处理的类进行遍历for (NSUInteger i = 0; i < self.DIClasses.count; i++) {NSString *className = self.DIClasses[i];Class class = NSClassFromString(className);unsigned int outCount;// 反射出所有属性objc_property_t *props = class_copyPropertyList(class, &outCount);// 保存所有注解属性,注解属性包含了位置索引(index)、名称(name)和类型(type),通过一个模型类SGAnnotation来存储NSMutableArray *annotations = @[].mutableCopy;// 保存所有的属性信息,每个属性包含了名称(name)和类型(type),通过一个模型类SGProperty来存储NSMutableArray *properties = @[].mutableCopy;// 遍历所有属性for (NSUInteger i = 0; i < outCount; i++) {objc_property_t prop = props[i];NSString *propName = [[NSString alloc] initWithCString:property_getName(prop) encoding:NSUTF8StringEncoding];// 这一段代码用于从描述属性的字符串中获取到类型,用到了正则和字串处理NSString *propAttrs = [[NSString alloc] initWithCString:property_getAttributes(prop) encoding:NSUTF8StringEncoding];NSRange range = [propAttrs rangeOfString:@"@\".*\"" options:NSRegularExpressionSearch];if (range.location != NSNotFound) {range.location += 2;range.length -= 3;NSString *typeName = [propAttrs substringWithRange:range];// 如果当前属性为注解属性,则记录进annotaionsif ([self isPropertyAnnotationByType:typeName]) {DSAnnotation *anno = [DSAnnotation new];anno.index = i;anno.name = propName;anno.type = typeName;[annotations addObject:anno];}// 记录每一条属性DSProperty *sp = [DSProperty new];sp.name = propName;sp.type = typeName;[properties addObject:sp];}} // scan class properties end// 从容器中得到类的实例id diInstance = [self getInstanceByClassName:className];NSMutableDictionary *mutableDic = [NSMutableDictionary dictionary];// 遍历注解,得到所有被修饰的属性for (NSUInteger i = 0; i < annotations.count; i++){DSAnnotation *annotation = annotations[i];DSProperty *prop = properties[annotation.index + 1];[mutableDic setObject:annotation.type forKey:prop.name];}[diInstance setValue:mutableDic forKey:@"dsUnitsAnnotationMapper"];} // scan classes end}
介绍
区别
objection_requires_sel和objection_initializer_sel分别对应属性注入和构造器注入),你还可以通过模块的概念将功能划分统一配置管理,绑定的类型丰富多样,但是 Objection 的侵入性较强。源码
Demo
EXTConcreteProtocol浅析,这个库主要就是通过协议来向Class注入对应的函数,但是需要遍历遵守协议的所有Class,从而实现动态注入。
一次高效的依赖注入,这个方式基本逻辑差不多,但这里不是通过遍历的方式,比如工程里有上万个类,那遍历的效率就会很低。所以这里的方案是在Class调用对应的函数找不到时进入消息转发,然后再实现动态注入:
1、开始我们在 +load 方法中做准备工作,把所有的协议都存到链表中。
2、在 __attribute__((constructor)) 中做是否能执行注入的检查。
3、现在我们 hook NSObject 的 +resolveInstanceMethod: 和 +resolveClassMethod: 。
4、在 hook 中进行检查,如果该类有遵守了我们实现了注入的协议,那么就给该类注入容器中的方法。
反射、注解与依赖注入总结
iOS控制反转(IoC)与依赖注入(DI)的实现
java注解是怎么实现的?
Android编译时注解框架系列1-什么是编译时注解