背景
最近接触了一段时间的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来配置要扫描的类的特征,目前提供了前缀和类列表,对于有公共前缀的类可以配置一个前缀来实现扫描,对于没有前缀的类,单独配置到类列表中,如下图所示。
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的基本实现,这只是一次探索,还有很多地方需要优化和完善。
网友评论