@qidiandasheng
2021-01-25T17:54:55.000000Z
字数 7730
阅读 3126
架构
控制反转(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];
// 如果当前属性为注解属性,则记录进annotaions
if ([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-什么是编译时注解