本文仅探讨 iOS 中单例的适用场景及生命周期管理,如需单例教程及其定义作用的请访问:设计模式系列14--单例模式。
最近在做项目的重构工作,翻看了一下源码,发现了各种历史遗留问题。其中随处可见的单例,产生了万物皆单例的现象(说好的万物皆对象呢?)。
在与前开发人员沟通后,对方坚持使用单例的原因如下:
- 代码简洁,不需要声明属性以及创建新的实例对象,需要的时候就可以马上调用。
- 方便管理对象的生命周期,把对象的创建和销毁时机都掌握在开发人员手中,可以控制对象的销毁时机。
- 历史遗留,iOS 系统类中随处可见的单例,我们的前辈们也都是这么用的,那就这么干吧。
第一点无法反驳,单例确实很好用,写起来有种欲仙欲死的快感。但是,不管副作用的话,毒品产生的快感大概比这更甚吧。作为一个有追求的程序猿,怎么能被普通的感官快感所诱惑,我们的目标是星辰大海好吗。
第二点无法直视,既然是单例为什么要手动销毁呢。这时候就有人说了,比如退出登录后,需要把账户的单例销毁。作为需要全局使用的对象,这样的需求确实无可厚非,那么如果这个单例对象只是在一个地方使用到了呢?需要特地建一个单例并手动去管理单例的释放时机吗?这还是单例吗,这是假单例吧。
真单例
吐槽完毕。进入正题,单例作为一个变态的全局变量,首先看他的定义:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
那么他的使用场景很简单且很明确:
在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在 APP 开发中我们可能在任何地方都要使用用户的信息,那么可以在登录的时候就把用户信息存放在一个文件里面,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
有的情况下,某个类可能只能有一个实例。比如说你写了一个类用来播放音乐,那么不管任何时候只能有一个该类的实例来播放声音。再比如,一台计算机上可以连好几个打印机,但是这个计算机上的打印程序只能有一个,这里就可以通过单例模式来避免两个打印任务同时输出到打印机中,即在整个的打印过程中我只有一个打印程序的实例。
综上所述,不遵守以上定义的单例都是伪单例,例如用户信息单例就是典型的伪单例。
伪单例
使用伪单例并没有什么错,我们不需要咬文爵字,只要有合适的应用场景,并承认自己是伪单例,我们也可以开开心心地使用它。
那么我们今天就来好好谈谈伪单例的正确使用姿势(不管是不是你创造的,既然接盘了你就要负责到底)。
首先本文中对伪单例的定义:
需要管理生命周期,并且长时间不需要销毁的单例对象。
即在单例对象的基础上,需要对其生命周期进行管理,并且在应用启动期间如没有特殊情况,会一直存活。
伪单例的销毁
伪单例的销毁要基于其创建的方式,常规的有两种:同步锁、GCD。
static InstanceSync *instance = nil;
@implementation InstanceSync
// 同步锁方式
+(instancetype)shareInstance{
@synchronized (self) {
if (!instance) {
instance = [[self alloc]init];
}
}
return instance;
}
static InstanceSync *instance = nil;
static dispatch_once_t onceToken;
@implementation InstanceSync
// GCD 方式
+(instancetype)shareInstance{
dispatch_once(&onceToken, ^{
instance = [[self alloc]init];
});
return instance;
}
首先我们使用同步锁的单例来试验一下,一般我们销毁一个对象是将其置为空,即可以释放,如下:
NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
InstanceSync *instanceSync = [InstanceSync shareInstance];
instanceSync = nil;
NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
实际上,这样并不能销毁这个对象:
2017-04-10 10:54:10.449 instanceSync : <InstanceSync: 0x600000016ea0>
2017-04-10 10:54:10.449 instanceSync : <InstanceSync: 0x600000016ea0>
其实在常规单例的内部都有一个全局静态变量,我们需要对其置空才能释放该单例对象:
-(void)destoryInstance{
instance = nil;
}
-(void)dealloc{
NSLog(@"%@ occur",NSStringFromSelector(_cmd));
}
那么我们再来尝试一下:
NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
InstanceSync *instanceSync = [InstanceSync shareInstance];
[instanceSync destoryInstance];
NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
2017-04-10 11:05:22.112 instanceSync : <InstanceSync: 0x608000200480>
2017-04-10 11:05:22.112 instanceSync : <InstanceSync: 0x600000200430>
2017-04-10 11:05:24.366 dealloc occur
可以看到伪单例对象 [InstanceSync shareInstance]
并没有马上进入 dealloc
,而是在打印完第二 log 后才进入 dealloc
;因此这里需要注意:
如果伪单例对象被外部变量所持有,那么在释放单例对象时,需要确保所有持有变量都被释放后,才可以进入单例的释放。因此不建议将单例赋值给外部变量,以免无法在预期内释放单例对象。
此外再次调用 [InstanceSync shareInstance]
将会产生新的对象,这也是易于理解的,那么如果使用 GCD 的方式能否产生新的对象?
实际上,这就取决于你销毁对象的方式:
-(void)destoryInstance{
instance = nil; // 销毁静态全局变量
onceToken = nil; // 销毁 GCD onceToken
}
如果只销毁静态全局变量,那么调用该方法后,将不会产生新的对象:
2017-04-10 11:21:37.917 instanceGCD : <InstanceGCD: 0x60000000d700>
2017-04-10 11:21:37.918 instanceGCD : (null)
2017-04-10 11:21:37.918 dealloc occur
如果销毁 GCD onceToken ,那么不论销毁静态全局变量,都会产生新的对象。
结束
实际上,本文讲述的是在明知是伪单例的情况下,如何正确地管理伪单例的生命周期,文中若有不实之处,希望大家提出宝贵的意见。
网友评论