本文介绍了一种名为依赖注入(Dependency Injection)的设计模式,并使用这种模式释放不必一直持有的对象,用来达到释放内存的效果。
什么是依赖注入
举个栗子:
“When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.
What you should be doing is stating a need, “I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.”
以上答案来自Stack Overflow,大概意思是,对于一个5岁大的小朋友来说,从冰箱里取东西出来是一件危险的事情,你可能忘记关冰箱门,或者取出爸爸的伏特加,也可能寻找根本不存在的食物或者用舌头舔冷冻室的铁栏杆……
因此,这件事的解决办法就是,对你的父母说:“我想在吃午饭时喝点什么”,然后父母就会拿出一些喝的给你。
换成程序员能听懂的话就是:高层类(5岁小孩)应该依赖底层基础设施(家长)来提供必要的服务。
依赖注入是一个将行为从依赖中分离的技术,简单地说,它允许开发者定义一个方法函数依赖于外部其他各种交互,而不需要编码如何获得这些外部交互的实例。 这样就在各种组件之间解耦,从而获得干净的代码,相比依赖的硬编码, 一个组件只有在运行时才调用其所需要的其他组件,因此在代码运行时,通过特定的框架或容器,将其所需要的其他依赖组件进行注入,主动推入。
为什么需要依赖注入
依赖注入框架的运用可以帮我们将APP的设计分割成好几个模块,分给不同的开人员,当完成开发之后再进行合并充分解决了团队之间模块化分工的不足。
借用objccn.io的话说:
我最初决定钻研DI是因为在执行测试驱动开发 (TDD),而在 TDD 的过程中有一个很纠结的问题会时常跳出来:“对于这个实现,如何编写单元测试?”。后来我发现其实 DI 本身是在彰显一个更高层面的概念:代码组成了模块,模块拼接构建成了应用本身。
如何实现依赖注入
以下例子来自objeccn.io。
构造器注入
构造器注入,即将某个依赖对象传入到构造器中 (在 Objective-C中指 designated 初始化方法) 并存储起来,以便在后续过程中使用:
- (NSNumber *)nextReminderId
{
NSNumber *currentReminderId = [self.userDefaults objectForKey:@"currentReminderId"];
if (currentReminderId) {
currentReminderId = @([currentReminderId intValue] + 1);
} else {
currentReminderId = @0;
}
[self.userDefaults setObject:currentReminderId forKey:@"currentReminderId"];
return currentReminderId;
}
属性注入
对于属性注入,nextReminderId
的代码看起来和 self.userDefaults
的做法是一致的。只是这次不是将依赖对象传递给初始化方法,而是采用属性赋值方式:
@interface Example
@property (nonatomic, strong) NSUserDefaults *userDefaults;
- (NSNumber *)nextReminderId;
@end
现在可以在单元测试中创建一个对象,然后将需要的东西通过对 userDefaults 属性进行赋值。但是要是这个属性没有被预先设定的话要怎么办呢?这时,我们可以使用 lazy 加载的方法为其设置一个适当的默认值,这能保证始终可以通过 getter 拿到一个确切的值:
- (NSUserDefaults *)userDefaults
{
if (!_userDefaults) {
_userDefaults = [NSUserDefaults standardUserDefaults];
}
return _userDefaults;
}
这样的话,对 userDefaults
来说,如果在使用者取值之前做过赋值操作,那么从 self.userDefaults
得到的就是通过 setter 赋的值。如果这个属性在使用前未被赋值,从 self.userDefaults 得到的就是[NSUserDefaults standardUserDefaults]
。
方法注入
如果依赖对象只在某一个方法中被使用,则可以利用方法参数做注入:
- (NSNumber *)nextReminderIdWithUserDefaults:(NSUserDefaults *)userDefaults
{
NSNumber *currentReminderId = [userDefaults objectForKey:@"currentReminderId"];
if (currentReminderId) {
currentReminderId = @([currentReminderId intValue] + 1);
} else {
currentReminderId = @0;
}
[userDefaults setObject:currentReminderId forKey:@"currentReminderId"];
return currentReminderId;
}
再一次说明,这样看起来可能会很奇怪,并不是所有的例子中 NSUserDefaults
作为依赖都显得恰如其分。比如说这个例子中,如果使用 NSDate
做注入参数传入可能更会彰显其特点。
如何实现内存释放
上面提到的几种实现依赖注入的方法中,可以用来释放不必一直持有的对象的方法是属性注入。如果在一个对象的生命周期中,有什么属性是可以随时从文件系统中(或是其他方法)恢复的,那就让我们注入它吧!
举个例子,设备横屏时显示红色按钮redButton
,设备竖屏时显示绿色按钮greenButton
,同时两个button的行为完全一致,对外暴露的接口可以统一为一个button
:
// 这是对外接口,属性注入
- (UIButton *)button
{
if(UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])) {
return self.redButton;
}
else {
return self.greenButton;
}
}
// 继续注入两个button
- (UIButton *)redButton
{
if(!_redButton) {
_redButton = [UIButton new];
// do sth.
}
return _redButton;
}
- (UIButton *)greenButton
{
if(!_greenButton) {
_greenButton = [UIButton new];
// do sth.
}
return _greenButton;
}
设备处于某个方向时,同时只有一个button被显示,因此可以释放掉不被显示的一个以节约内存
- (UIButton *)removeHiddenButton
{
if(UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])) {
[_greenButton removeFromSuperView];
_greenButton = nil;
}
else {
[_redButton removeFromSuperView];
_redButton = nil;
}
}
- (void)layoutSubviews
{
[self button];
[self removeHiddenButton];
}
属性注入保证了调用者(高级类)不必关心被调用对象的初始化情况,所以即使被调用对象已经释放,通过调用注入接口,仍然可以在需要时生成。利用这一特性,我们可以在收到内存警告时释放掉暂时不用的对象,同时也必须注意,被释放的对象必须是可恢复的。
网友评论