伪单例模式

本文仅探讨 iOS 中单例的适用场景.

如需单例教程及其定义作用的请访问:http://www.jianshu.com/p/5226bc8ed784

最近在做项目的重构工作,翻看了一下源码,发现了各种历史遗留问题。其中随处可见的单例,产生了万物皆单例的现象(说好的万物皆对象呢?)。

在与前开发人员沟通后,对方坚持使用单例的原因如下:

  • 代码简洁,不需要声明属性以及创建新的实例对象,需要的时候就可以马上调用。
  • 方便管理对象的生命周期,把对象的创建和销毁时机都掌握在开发人员手中,可以控制对象的销毁时机。
  • 历史遗留,iOS 系统类中随处可见的单例,我们的前辈们也都是这么用的,那就这么干吧。

第一点无法反驳,单例确实很好用,写起来有种欲仙欲死的快感。但是,不管副作用的话,毒品产生的快感大概比这更甚吧。作为一个有追求的程序猿,怎么能被普通的感官快感所诱惑,我们的目标是星辰大海好吗。

第二点无法直视,既然是单例为什么要手动销毁呢。这时候就有人说了,比如退出登录后,需要把账户的单例销毁。作为需要全局使用的对象,这样的需求确实无可厚非,那么如果这个单例对象只是在一个地方使用到了呢?需要特地建一个单例并手动去管理单例的释放时机吗?这还是单例吗,这是假单例吧。

真单例

吐槽完毕。进入正题,单例作为一个变态的全局变量,首先看他的定义:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

那么他的使用场景很简单且很明确:

  • 在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在 APP 开发中我们可能在任何地方都要使用用户的信息,那么可以在登录的时候就把用户信息存放在一个文件里面,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

  • 有的情况下,某个类可能只能有一个实例。比如说你写了一个类用来播放音乐,那么不管任何时候只能有一个该类的实例来播放声音。再比如,一台计算机上可以连好几个打印机,但是这个计算机上的打印程序只能有一个,这里就可以通过单例模式来避免两个打印任务同时输出到打印机中,即在整个的打印过程中我只有一个打印程序的实例。

综上所述,不遵守以上定义的单例都是伪单例,例如用户信息单例就是典型的伪单例。

伪单例

使用伪单例并没有什么错,我们不需要咬文爵字,只要有合适的应用场景,并承认自己是伪单例,我们也可以开开心心地使用它。

那么我们今天就来好好谈谈伪单例的正确使用姿势(不管是不是你创造的,既然接盘了你就要负责到底)。

首先本文中对伪单例的定义:

需要管理生命周期,并且长时间不需要销毁的单例对象。

即在单例对象的基础上,需要对其生命周期进行管理,并且在应用启动期间如没有特殊情况,会一直存活。

伪单例的销毁

伪单例的销毁要基于其创建的方式,常规的有两种:同步锁、GCD。

1
2
3
4
5
6
7
8
9
10
11
static InstanceSync *instance = nil;
@implementation InstanceSync
// 同步锁方式
+(instancetype)shareInstance{
@synchronized (self) {
if (!instance) {
instance = [[self alloc]init];
}
}
return instance;
}
1
2
3
4
5
6
7
8
9
10
static InstanceSync *instance = nil;
static dispatch_once_t onceToken;
@implementation InstanceSync
// GCD 方式
+(instancetype)shareInstance{
dispatch_once(&onceToken, ^{
instance = [[self alloc]init];
});
return instance;
}

首先我们使用同步锁的单例来试验一下,一般我们销毁一个对象是将其置为空,即可以释放,如下:

1
2
3
4
NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
InstanceSync *instanceSync = [InstanceSync shareInstance];
instanceSync = nil;
NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);

实际上,这样并不能销毁这个对象:

1
2
2017-04-10 10:54:10.449 instanceSync : <InstanceSync: 0x600000016ea0>
2017-04-10 10:54:10.449 instanceSync : <InstanceSync: 0x600000016ea0>

其实在常规单例的内部都有一个全局静态变量,我们需要对其置空才能释放该单例对象:

1
2
3
4
5
6
7
-(void)destoryInstance{
instance = nil;
}
-(void)dealloc{
NSLog(@"%@ occur",NSStringFromSelector(_cmd));
}

那么我们再来尝试一下:

1
2
3
4
NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
InstanceSync *instanceSync = [InstanceSync shareInstance];
[instanceSync destoryInstance];
NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
1
2
3
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 的方式能否产生新的对象?

实际上,这就取决于你销毁对象的方式:

1
2
3
4
-(void)destoryInstance{
instance = nil; // 销毁静态全局变量
onceToken = nil; // 销毁 GCD onceToken
}

如果只销毁静态全局变量,那么调用该方法后,将不会产生新的对象:

1
2
3
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 ,那么不论销毁静态全局变量,都会产生新的对象。

结束

实际上,本文讲述的是在明知是伪单例的情况下,如何正确地管理伪单例的生命周期,文中若有不实之处,希望大家提出宝贵的意见。