1: 单例的定义
在程序运行的整个过程中,根据类创建的所有的对象以及 copy 的对象,其内存地址都是一样的,也就是说,只要是单例,那么系统就只会创建一次, 只会给其分配一次存储空间;
2: 单例的作用
根据单例模式的定义,我们知道一般两种情况下使用单例:
-
系统中某种对象只能存在一个,多了就会出问题
-
系统中某种对象实例只需要一个就够用了,多了占内存
对于第一种情况,我们必须使用单例,对于第二种情况,我们虽然可以不用单例,但是单例是更优的选择
3: 单例的使用场景
单例在整个项目过程中,共享一份资源 系统API提供的的单例有
[NSUserDefaults standardUserDefaults];
[NSNotificationCenter defaultCenter];
[UIApplication sharedApplication];
[NSFileManager defaultManager]
4: 单例的创建
- 写法1.
不考虑多线程的单例创建方式
+ (instancetype)shareInstance {
static SingleInstance *_instance;
if(!single) {
single = [[SingleInstance alloc] init];
}
return _instance;
}
严格意义上来说,我们还需要将alloc方法封住,因为严格的单例是不允许再创建其他实例的,而alloc方法可以在外部任意生成实例。但是考虑到alloc属于NSObject,iOS中无法将alloc变成私有方法,最多只能覆盖alloc让其返回空,不过这样做也可能会让使用接口的人误解,造成其他问题。所以我们一般情况下对alloc不做特殊处理。系统的单例也未对alloc做任何处理
对于一个实例,我们一般并不能保证他一定会在单线程模式下使用,所以我们得适配多线程情况。在多线程情况下,上面的单例创建方式可能会出现问题。如果两个线程同时调用shareInstance,可能会创建出2个single来。所以对于多线程情况下,我们可以使用@synchronized来加锁。
- 写法2: 加
互斥锁,解决多线程访问安全问题
+(id)sharedInstance {
// 创建静态对象 防止外部访问
static SingleInstance *_instance;
@synchronized (self) {
// 为了防止多线程同时访问对象,造成多次分配内存空间,所以要加上线程锁。
// 相比于GCD,@synchronized比较耗性能
if (instance == nil) {
instance = [[super allocWithZone:NULL] init];
}
return _instance;
}
这样的话,当多个线程同时调用shareInstance时,由于@synchronized已经加锁,所以只能有一个线程进入创建_instance。这样就解决了多线程下调用单例的问题.
使用@synchronized虽然解决了多线程的问题,但是并不完美。因为只有在instance未创建时,我们加锁才是有必要的。如果instance已经创建.这时候锁不仅没有好处,而且还会影响到程序执行的性能(多个线程执行@synchronized中的代码时,只有一个线程执行,其他线程需要等待. 而且@synchronized在底层增删改查消耗了大量性能.
- 写法3:
GCD dispatch_onec, 本身是线程安全的,保证整个程序中只会执行一次
+ (instancetype)sharedInstance {
static ClassName *_instance;
static dispatch_one_t oneToken;
dispatch_once(&onetoken,^{
_instance = [self alloc]init];
});
return _instance;
}
+ (instancetype)allocWithZone:(NSZone *) zone{
static dispatch_t onetoken;
dispatch_once(&oncetoken ^{
_instance = [super allocwithzone:zone];
})
retun _instance
}
static dispatch_once_t onceToken的原理:
dispatch_once_t
It must be initialized to zero.
* Note: static and global variables default to zero.
多线程解决当前创建对象的时候,保证只有一条线程执行创建代码,是根据onceToken的值来判断的,默认它的值为0,也可以通过在不同的位置,打印它的值,来验证它什么时候执行
NSLog(@"已经执行了%ld",onceToken);
- 当
onceToken= 0时,线程执行dispatch_once的block中代码; - 当
onceToken= -1时,线程跳过dispatch_once的block中代码不执行; - 当
onceToken为其他值时,被线程被阻塞,等待onceToken值改变; 当线程第一次调用sharedInstance,onceToken的值为0,执行block中的代码, 当执行时,系统内部将onceToken的值修改为140734629902336,当执行完成后,将onceToken修改为-1;以下就分两种情况了:
第一种情况:
某一线程要执行block中的代码时,首先需要判断onceToken的值,再去执行block中的代码。一旦有线程首次正在执行,那么这里onceToken的值变为140734731430192,也就表明当前线程正在执行,其他线程被阻塞;
当当前线程执行完block之后, onceToken变为-1,那么, 当有其他线程执行时,获取到的onceToken值为-1,其他线程直接跳过block, 保证只会创建一次。
第二种情况:
一旦执行过一次,下次再调用sharedInstance时,block已经为-1。直接跳过block。