背景

最近接触了一段时间的SpringMVC,对其控制反转(IoC)和依赖注入(DI)印象深刻,此后便一直在思考如何使用OC语言较好的实现这两个功能。Java语言自带的注解特性为IoC和DI带来了极大的方便,要在OC上较好的实现这两个功能,需要一些小小的技巧。

控制反转和依赖注入

控制反转

简单来说,将一个类对象的创建由手动new方式改为从IOC容器内获取,就是一种控制反转,例如我们现在要创建一个ClassA类,则常规方法为

ClassA *a = [ClassA new];

如果使用控制反转,则从容器内获取ClassA对象,对象由容器负责创建。

ApplicationContext *context = [ApplicationContext sharedContext];
ClassA *a = [context getInstanceByClassName:@"ClassA"];

依赖注入

所谓依赖注入,是指一个类对象不应负责去查找其依赖属性,而是交由容器去处理。例如ClassA类有一个ClassB类对象,如下所示。

@interface ClassA : NSObject

@property(nonatomic, strong) ClassB *b;

@end

常规情况下,要将ClassB的对象创建后传递给ClassA,如下所示。

ClassA *a = [ClassA new];
ClassB *b = [ClassB new];
a.b = b;

如果交由容器处理,则容器会自动创建ClassA、ClassB,并且根据ClassA的依赖属性类型ClassB自动的将ClassB的实例注入到ClassA对象中。

优点

使用控制反转和依赖注入将对象的创建与依赖对象的注入交由IoC容器来完成,这样做不仅能够降低代码的耦合度,更可以减少代码量。

类与属性的修饰

这两个功能都是在运行时通过反射来实现的,具体的容器技术细节在下文讨论,这里主要讨论的是如何修饰交由容器创建的对象以及依赖注入的属性。

Java中的注解

在Java中,那些要交由Spring容器创建的类通过注解来修饰,例如JavaWeb中的Controller和Service分别由@Controller和@Service修饰,如下所示。

// 控制器
@Controller
public class SomeController {...}
// 服务
@Service
public class SomeService {...}

而控制器要通过服务来处理业务逻辑,因此Controller依赖了Service,通过@Autowired修饰这一属性,即可完成自动注入,如下所示。

@Controller
public class AdministratorController {
@Autowired
private SomeService serv;
...
}

通过@Autowired注解,IoC容器会根据属性类型(SomeService)找到依赖对象的实例,并且注入到Controller的serv属性。这一过程中,Controller、Service以及注入均不需要写额外的代码,只需要通过注解修饰即可。

OC中的实现

由于OC没有注解特性,因此要标记类和属性就需要其他的方法,我经过思考发现了一种较好的方式,那就是通过协议来标记类,通过额外的属性来标记属性。

* 为了模仿@Controller、@Service等修饰类的注解,我们使用IOCComponents协议来标记类,来表示这些类交由容器处理。

@interface SGService : NSObject <IOCComponents>
...
@end
  • 为了模仿@Autowired实现的按类型注入,我们在要注入的属性前多加一条属性,该条属性的类型为TypeAnnotation,我们称之为注解属性,这个类通过@Class声明而并不存在,只是为了标记,在反射时得到的所有属性都是按照顺序排列的,因此每一条注解属性后的属性都是需要进行依赖注入的,示例如下。
@interface SGService : NSObject <IOCComponents>

@property (nonatomic, weak, readonly) TypeAnnotation *autowired_0;
@property (nonatomic, strong) SGMapper *mapper; @end

通过上文这种方式,在反射时autowired_0和mapper是相邻的,因此可以判断出mapper需要注入,只需要反射出该property的信息,并且从容器中查找,并注入即可,为了方便定义这样的注解属性,我们使用一个宏如下所示。

#define Autowired(num) @property (nonatomic, weak, readonly) TypeAnnotation *autowired_##num;

那么上面的代码可以进行如下的简化。

@interface SGService : NSObject <IOCComponents>

Autowired(0)
@property (nonatomic, strong) SGMapper *mapper; @end

这里的数字是为了多个注解造成属性的重复定义,可以从0开始编号。

OC的一个实现示例

这里假设IoC容器已经完成,主要演示流程,容器的技术细节在下文讨论。

有一个Service类和一个Mapper类,Service依赖了Mapper,具体代码如下。

类的定义与修饰

  • IOCComponents用于标识该类由容器负责创建
  • Autowired(x)表示该属性按类型进行依赖注入
@interface SGService : NSObject <IOCComponents>

Autowired(0)
@property (nonatomic, strong) SGMapper *mapper; @end
@interface SGMapper : NSObject <IOCComponents>

@end

扫描配置

类似于Spring的applicationContext.xml,这里通过ApplicationContext.plist来配置要扫描的类的特征,目前提供了前缀和类列表,对于有公共前缀的类可以配置一个前缀来实现扫描,对于没有前缀的类,单独配置到类列表中,如下图所示。

验证与使用

经过上面的配置,IoC容器便可完成Service与Mapper的创建,以及将Mapper注入到Service,验证如下。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
SGApplicationContext *context = [SGApplicationContext sharedContext];
SGService *serv = [context getInstanceByClassName:@"SGService"];
printf("%s -> %s\n",serv.description.UTF8String, serv.mapper.description.UTF8String);
return YES;
}

打印如下

<SGService: 0x7ff34bc749f0> -> <SGMapper: 0x7ff34bc74920>

IoC容器的实现

IoC容器的核心功能是对象创建、依赖查找和依赖注入,这些功能都需要借助运行时的反射实现,下面将按照容器初始化的过程来逐一介绍用到的OC运行时函数,这些函数均可以在包含objc/runtime.h后使用。

1.IoC容器

容器通过SGApplicationContext单例来管理需要容器创建的类对象,通过一个Dictionary类型的属性instanceMap存储对象是否已经创建过,容器中的对象目前还只是单例的,逻辑很简单,每次根据类型取出对象时,先检查instanceMap该对象是否已经创建过,如果创建过则直接返回,否则创建后返回,这里会通过运行时函数objc_getClass检查该类是否已经装载,具体实现如下。

- (id)getInstanceByClassName:(NSString *)className {
if (self.instanceMap[className] == nil) {
Class clazz = NSClassFromString(className);
// 检查类是否已经装载,防止未定义的类实例化时出错
if (!objc_getClass(className.UTF8String)) {
return nil;
}
id instance = [clazz new];
self.instanceMap[className] = instance;
}
return self.instanceMap[className];
}

2.扫描类列表

通过函数objc_getClassList可以获取类列表,该函数的具体信息如下。

int objc_getClassList(Class *buffer, int bufferCount);

buffer是所有类的数组,bufferCount为数组大小,返回值也为数组大小,由于第一次调用时并不知道bufferCount,因此可以两次调用,第一次拿到bufferCount,第二次再初始化类列表数组,具体代码如下,详细请见注释。

- (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);
// 第一个判断条件对应于ApplicationContext.plist中的扫描类特征配置,只有符合条件的类才能被添加
// 第二个条件检查IOCComponents标记,有标记的类才被IoC容器处理
if ([self isValidIOCClassNamed:className] && [clazz conformsToProtocol:protocol]) {
[temp addObject:className];
}
}
// 将IoC需要处理的类存储起来
self.DIClasses = temp;
// 由于类列表是malloc创建的,需要手动释放
free(classList);
// 根据注解属性处理依赖注入
[self scanAnnotation];
}

3.扫描类属性与属性注入

要扫描一个类的所有私有和公有属性,使用下面的函数。

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount);

该方法可以列出所有属性,先私有后公有,按照定义的顺序从上到下列出,其返回值是一个objc_property_t结构体数组,从objc_property_t结构体中可以得到属性的各种信息,对于名称可以通过property_getName函数直接获得,而其他信息需要先通过property_getAttributes方法获得描述属性的字符串,再进行分割。通过这样的步骤,即可获得所有注解属性的位置,并由此得到所有需要注入的属性,根据这些属性的类型进行注入即可。

要完成注入,需要先得到代表属性的Ivar,然后对实例进行注入,得到Ivar和注入属性的函数如下。

// 注意得到Ivar时的属性名为实例变量名而不是property名,例如@property定义的xx,则这里应该写作_xx
Ivar class_getInstanceVariable(Class cls, const char *name);
// 将value注入到obj实例的 ivar实例变量上
void object_setIvar(id obj, Ivar ivar, id value);

具体代码如下,详细请看注释。

- (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 ([typeName isEqualToString:@"TypeAnnotation"]) {
SGAnnotation *anno = [SGAnnotation new];
anno.index = i;
anno.name = propName;
anno.type = typeName;
[annotations addObject:anno];
}
// 记录每一条属性
SGProperty *sp = [SGProperty new];
sp.name = propName;
sp.type = typeName;
[properties addObject:sp];
}
} // scan class properties end
// 从容器中得到类的实例
id diInstance = [self getInstanceByClassName:className];
// 遍历注解,得到所有被修饰的属性
for (NSUInteger i = 0; i < annotions.count; i++){
SGAnnotation *annotation = annotations[i];
SGProperty *prop = properties[annotation.index + 1];
NSString *typeName = prop.type;
NSString *varName = prop.name;
// 得到依赖对象,并注入到注解修饰的属性
id destInstance = [self getInstanceByClassName:typeName];
Ivar ivar = class_getInstanceVariable([diInstance class], [NSString stringWithFormat:@"_%@",varName].UTF8String);
object_setIvar(diInstance, ivar, destInstance);
}
} // scan classes end
}

总结

通过这样的处理,完成了OC语言对IoC和DI的基本实现,这只是一次探索,还有很多地方需要优化和完善。

iOS控制反转(IoC)与依赖注入(DI)的实现的更多相关文章

  1. 控制反转IOC与依赖注入DI

    理解 IOC  http://www.cnblogs.com/zhangchenliang/archive/2013/01/08/2850970.html IOC 相关实例      的http:// ...

  2. 控制反转(Ioc)和依赖注入(DI)

    控制反转IOC, 全称 “Inversion of Control”.依赖注入DI, 全称 “Dependency Injection”. 面向的问题:软件开发中,为了降低模块间.类间的耦合度,提倡基 ...

  3. 轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI) 依赖注入和控制反转的理解,写的太好了。

    轻松学,浅析依赖倒置(DIP).控制反转(IOC)和依赖注入(DI) 2017年07月13日 22:04:39 frank909 阅读数:14269更多 所属专栏: Java 反射基础知识与实战   ...

  4. 控制反转IOC与依赖注入DI【转】

    转自:http://my.oschina.net/1pei/blog/492601 一直对控制反转.依赖注入不太明白,看到这篇文章感觉有点懂了,介绍的很详细. 1. IoC理论的背景我们都知道,在采用 ...

  5. 【转载】浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI)

    原文地址 http://blog.csdn.net/briblue/article/details/75093382 写这篇文章的原因是这两天在编写关于 Dagger2 主题的博文时,花了大量的精力来 ...

  6. 控制反转IOC与依赖注入DI - 理论篇

    学无止境,精益求精 十年河东十年河西,莫欺少年穷 昨天是五一小长假归来上班的第一天,身体疲劳,毫无工作热情.于是就看看新闻,喝喝茶,荒废了一天 也就在昨天,康美同事张晶童鞋让我学习下IOC的理论及实现 ...

  7. 依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI)

    原文: https://blog.csdn.net/briblue/article/details/75093382 写这篇文章的原因是这两天在编写关于 Dagger2 主题的博文时,花了大量的精力来 ...

  8. 20181123_控制反转(IOC)和依赖注入(DI)

    一.   控制反转和依赖注入: 控制反转的前提, 是依赖倒置原则, 系统架构时,高层模块不应该依赖于低层模块,二者通过抽象来依赖 (依赖抽象,而不是细节) 如果要想做到控制反转(IOC), 就必须要使 ...

  9. Spring框架学习笔记(1)——控制反转IOC与依赖注入DI

    Spring框架的主要作用,就是提供了一个容器,使用该容器就可以创建并管理对象.比如说Dao类等,又或者是具有多依赖关系的类(Student类中包含有Teacher类的成员变量) Spring有两个核 ...

随机推荐

  1. Checkbutton

    #tkinter之Checkbutton篇 #Checkbutton又称为多选按钮,可以表示两种状态,On和Off,可以设置回调函数,每当点击此按钮时回调函数被调用. 1.一个简单的Checkbutt ...

  2. Array方面Js底层代码学习记录

    一..clear() →Array function clear() { this.length = 0; return this; } 返回清除item的空数组. 例子: var fruits = ...

  3. C# 关键字替换

    /// <summary> /// 关键字替换 /// </summary> /// <param name="body"></param ...

  4. WPF 自定义TreeView控件样式,仿QQ联系人列表

    一.前言 TreeView控件在项目中使用比较频繁,普通的TreeView并不能满足我们的需求.因此我们需要滴对TreeView进行改造.下面的内容将介绍仿QQ联系人TreeView样式及TreeVi ...

  5. iOS 检测屏幕是否锁定 🔓 / 🔒

    1. 导入头文件 #import <notify.h> 2. 给 CFNotificationCenter 添加观察者 - (void)addLockStatusObserver { CF ...

  6. java模式之模板模式——抽象类

    模板设计模式(Template ) abstract class Action{ // 定义一个行为类 public static final String WORK = "work&quo ...

  7. EventBus InMemory 的实践基于eShopOnContainers (二)

    前言 最近在工作中遇到了一个需求,会用到EventBus,正好看到eShopOnContainers上有相关的实例,去研究了研究.下面来分享一下用EventBus 来改造一下我们上篇Event发布与实 ...

  8. [UOJ UNR#2 黎明前的巧克力]

    来自FallDream的博客,未经允许,请勿转载,谢谢. 传送门 很奇妙的一道题 首先不难发现一个暴力做法,就是f[i]表示异或和为i的答案数,每次FWT上一个F数组,其中F[0]=1,F[ai]=2 ...

  9. [BZOJ]1014 火星人prefix(JSOI2008)

    一边听省队dalao讲课一边做题真TM刺激. BZOJ的discuss简直就是题面plus.大样例.SuperHINT.dalao题解的结合体. Description 火星人最近研究了一种操作:求一 ...

  10. 关于HttpClient重试策略的研究

    一.背景 由于工作上的业务本人经常与第三方系统交互,所以经常会使用HttpClient与第三方进行通信.对于交易类的接口,订单状态是至关重要的. 这就牵扯到一系列问题: HttpClient是否有默认 ...