参考文献
iOS-单例的正确写法
iOS 单例的几种写法
CocoaChina里的讨论
NSObject官方文档
iOS 单例优缺点
介绍单例
- 单例类,在整个项目中只有一个实例,并提供一个类方法供全局调用,在编译时初始化这个类,然后一直保存在内存中,到程序退出时由系统自动释放这部分内存
- 一般在程序中,经常调用的类,如工具类、公共跳转类等都会采用单例模式
- 在程序中,一个单例类在程序中只能初始化一次,为了保证在使用中始终都是存在的,所以单例是在存储器的全局区域,在编译时分配内存,只要程序还在运行就会一直占用内存,在APP结束后由系统释放这部分内存
单例的代码实现
不使用GCD的方式
static Manager *manager;
@implementation Manager
+ (Manager *)sharedManager {
if(!manager) {
manager=[[self allocWithZone:NULL] init];
}
return manager;
}
+ (id)allocWithZone:(NSZone *)zone {
return [[self sharedManager] retain];
}
@end
注意: 此代码只适用于单线程,如果在多线程中使用将会创建多个实例
使用GCD
#import "Manager.h"
@implementation Manager
+ (Manager *)sharedManager {
static dispatch_once_t onceToken;
static Manager *sharedManager;
dispatch_once(&onceToken, ^{
sharedManager=[[Manager alloc] init];
});
return sharedManager;
}
@end
注: dispatch_once 无论使用多线程还是单线程,都只执行一次, 在安全的前提下也保证了性能, 是官方推荐的方式.
dispatch_once 主要是根据onceToken 的值来决定怎么去执行代码
- 当onceToken=0时,线程执行dispatch_once的block中代码
- 当onceToken=-1时,线程跳过dispatch_once的block中代码不执行
- 当onceToken为其他值时,线程被阻塞,等待onceToken值改变
dispatch_once 执行的流程:
- 当线程调用sharedManager,此时onceToken = 0,调用block中的代码。 此时onceToken的值变为140734537148864
- 当其他线程再调用sharedManager方法时,onceToken的值已经是140734537148864了,线程阻塞
- 当block线程执行完block之后,onceToken变为-1.其他线程不再阻塞,跳过block
- 下次再调用sharedManager时,block已经为-1.直接跳过block
关于allocWithZone:
问题来源于上文不使用GCD的单例代码实现
有人提出:为什么要覆盖allocWithZone:方法,到底 alloc 和 allocWithZone: 有什么区别呢
首先我们知道,我们需要保证单例类只有一个唯一的实例,而平时我们在初始化一个对象的时候, [[Class alloc] init],其实是做了两件事:alloc 给对象分配内存空间,init是对对象的初始化,包括设置成员变量初值这些工作。而给对象分配空间,除了alloc方法之外,还有另一个方法: allocWithZone:
在NSObject 这个类的官方文档里面,allocWithZone:方法介绍说,该方法的参数是被忽略的,正确的做法是传nil或者NULL参数给它。而这个方法之所以存在,是历史遗留原因
而实践证明,使用alloc方法初始化一个类的实例的时候,默认是调用了 allocWithZone: 的方法。于是覆盖allocWithZone:方法的原因已经很明显了:为了保持单例类实例的唯一性,需要覆盖所有会生成新的实例的方法,如果有人初始化这个单例类的时候不走[[Class alloc] init] ,而是直接 allocWithZone:, 那么这个单例就不再是单例了,所以必须把这个方法也堵上
单例的优缺点
主要优点:
- 提供了对唯一实例的受控访问
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能
- 允许可变数目的实例
主要缺点:
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难
- 单例类的职责过重,在一定程度上违背了“单一职责原则”
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失