设计模式第二篇、单例设计模式
目录
1、什么是单例设计模式
2、单例设计模式的简单实现
3、单例设计模式面临的两个问题及其完整实现
4、单例设计模式的应用场景
1、什么是单例设计模式
单例设计模式是指在App的整个生命周期中,某个类的实例只可能被创建一个,同时该类需要向外界提供一个sharedInstance方法来创建这个实例。
所以单例设计模式有三个设计思想:一不死性,即单例对象的生命周期和App的生命周期一样,只有在App杀死的时候单例对象才会被销毁;二唯一性,即在App的整个生命周期中单例对象只可能被创建一个;三、我们需要向外界提供一个sharedInstance方法来创建这个实例。
2、单例设计模式的简单实现
要实现单例设计模式,我们只需要抓住它的三个设计思想就可以了。
- 针对特点三,我们向外界提供一个创建单例的方法:
+ (instancetype)sharedSingleton;
-
针对特点一,我们想到全局变量的生命周期就是App的整个生命周期,所以可以采用全局变量来保证单例的不死性。
-
针对特点二,我们想到平常使用的懒加载就能保证只创建一个对象,所以采用懒加载(在这里是判断全局变量是否已经指向了对象)来保证单例的唯一性。
所以写代码如下:
ProjectSingleton *singleton = nil;
+ (instancetype)sharedSingleton {
if (singleton == nil) {
singleton = [[ProjectSingleton alloc] init];
}
return singleton;
}
但是使用全局变量来指向单例对象有一个很明显弱点是:外界使用extern关键字,可以很随便的就能修改掉这个单例对象,类型甚至都可以不是单例类的类型,那这不炸了嘛。如下:
extern ProjectSingleton *singleton;
singleton = @"222";
你看单例对象本来是ProjectSingleton类的实例,现在外界直接把它改成了字符串,都没报错。所以我们要把全局变量改成静态全局变量,这样这个变量就只能在单例类内部访问了,外界访问会直接报错。
static ProjectSingleton *singleton = nil;
+ (instancetype)sharedSingleton {
if (singleton == nil) {
singleton = [[ProjectSingleton alloc] init];
}
return singleton;
}
这样看起来好像没错了,单例设计模式的三个设计思想就算实现了。
3、单例设计模式面临的两个问题及其完整实现
上一小节,我们实现了单例设计模式的三个设计思想,但其实这只能算是简单的实现,因为这样实现单例会面临两个重要的问题:多线程问题和多创建入口问题,这两个问题都会导致单例的唯一性被破坏,我们需要慎重解决。
(1)多线程问题
如果按照上面的方式实现单例,那当我们的代码中有多个线程同时访问单例的创建方法+ (instancetype)sharedSingleton;
时,我们是不能保证单例的唯一性的。比如说线程一已经访问到了singleton = [[ProjectSingleton alloc] init];
创建单例对象这一步,但是还没有创建出来,此时线程二恰好正在访问if (singleton == nil)
这句代码,那么线程二得到的结果将是YES,也会走创建单例对象的代码,这样我们就会创建两个单例对象,违背了单例设计模式唯一性的设计思想。
因此,我们需要在懒加载这一步加锁,通过懒加载+锁的组合来保证多线程场景下单例创建的唯一性。当然了加锁的方式也有好几种,如@synchronized
、NSLock
和GCD信号semaphore
,由于@synchronized
的性能更高,所以我们用它。如下:
static ProjectSingleton *singleton = nil;
+ (instancetype)sharedSingleton {
@synchronized(self) {
if (singleton == nil) {
singleton = [[ProjectSingleton alloc] init];
}
}
return singleton;
}
但其实我们在这里并没有采用懒加载+锁的方式来保证单例在多线程场景下的唯一性,而是采用GCD的dispatch_once
,一方面它是线程安全的(可以替代锁的功能),另一方面它可以保证某段代码在整个App生命周期中只执行一次(可以替代懒加载)。这么做主要是因为用dispatch_once
来保证线程安全要比加锁的性能高几十倍(详见此文章),它的性能之所以如此高,是因为它的内部并不是靠pthread等锁来实现的,而是通过大量的原子操作来实现,这些原子操作直接用的是锁的汇编指令,靠CPU指令来支持。这样我们就用dispatch_once
代替了原来懒加载的设计方案来保证单例的唯一性,也同时保证了多线程场景下单例的唯一性。如下:
static ProjectSingleton *singleton = nil;
+ (instancetype)sharedSingleton {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
singleton = [[ProjectSingleton alloc] init];
});
return singleton;
}
(2)多创建入口问题
好了,通过GCD的dispatch_once
我们解决了单例创建的唯一性问题,但真得解决完了吗?没有啊,因为别人完全可以不用我们提供的+ (instancetype)sharedSingleton;
方法来创建单例,而是用系统提供的alloc、new、copy、mutableCopy来创建,这样的话我们在外界使用单例时是不能完全确保单例的唯一性的。
因此,我们需要把系统的这些创建入口给封死。我们知道调用alloc、new的时候会调用+ (instancetype)allocWithZone:(struct _NSZone *)zone;
方法,调用copy和mutableCopy的时候会调用- (instancetype)copyWithZone:(NSZone *)zone;
和- (instancetype)mutableCopyWithZone:(NSZone *)zone;
方法,所以我们可以在单例类里重写这些方法,让它们调用我们的+ (instancetype)sharedSingleton;
方法来返回单例对象就可以了。如下:
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [ProjectSingleton sharedSingleton];
}
- (instancetype)copyWithZone:(NSZone *)zone {
return [ProjectSingleton sharedSingleton];
}
- (instancetype)mutableCopyWithZone:(NSZone *)zone {
return [ProjectSingleton sharedSingleton];
}
哇,终于可以松口气了,其实解决了这两个问题之后我们才算完成了单例设计模式的实现。所以一个要实现完整的单例,代码应该如下:
@implementation ProjectSingleton
static ProjectSingleton *singleton = nil;
+ (instancetype)sharedSingleton {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
singleton = [[ProjectSingleton alloc] init];
});
return singleton;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [ProjectSingleton sharedSingleton];
}
- (instancetype)copyWithZone:(NSZone *)zone {
return [ProjectSingleton sharedSingleton];
}
- (instancetype)mutableCopyWithZone:(NSZone *)zone {
return [ProjectSingleton sharedSingleton];
}
- (instancetype)init {
self = [super init];
if (self != nil) {
// 一些属性的设置
}
return self;
}
@end
4、单例设计模式的应用场景
单例设计模式的应用场景也是由它的设计思想所决定的,单例不是具有唯一性和不死性嘛,所以我们可以得到它的应用场景:
-
用到唯一性:如果我们发现App的生命周期中只需要创建一个某个类的对象,那么这个类可以考虑使用单例设计模式,比如一些工具manager类就常用单例来实现。
-
用到不死性:如果某些对象的生命周期需要和App一样,或者我们想要延长某些对象的生命周期,就可以考虑使用单例设计模式。
-
单例传值
-
一对多传值:如果有一些数据在App中有很多地方都要用到,并且用不着持久化,我们就可以考虑使用单例来传值,这让我们感觉单例传值更像是一种一对多的传值方案。(比如说我们要根据后台返回来的数据动态的更换App的主题色,那么我们就可以把主题色作为一个成员变量放在单例类里,这样我们只需要做一次请求得到颜色,就可以在App的所有界面里使用这个颜色了。)
-
跨层传值:有的情况下,可能某些数据也并非是很多地方共享,也是一对一的传值,但是由于代理和block传值只适用于近层传值,所以实现不了,此时我们也可以考虑使用单例来实现这种跨层传值。(比如说我们在navigationController栈中第三层的控制器中做操作会影响第一层的某些行为,那么我们使用代理和block传值是无法隔层直接实现的,这是就可以考虑使用单例传值。)
-
但是我们也要慎重的使用单例设计模式,不能想什么时候用就什么时候用,特别是不要大材小用,要在真得有必要的时候才用,因为单例对象的生命周期很长,它内部的变量一旦强引用了外部的大对象,这些对象就只能等到App结束时才会被销毁,很容易造成内存问题。