1.单例的概念
在iOS中,单例的概念指的是在整个应用程序的生命周期内,单例对象的类必须保证只有一个实例对象存在。比如我们最常用的AFN,在我们的整个网络调用的过程中只有一个网络对象存在。再比如一个音乐播放器,在它运行的过程中必须保证只有一个实例在播放音乐。
2.单例的优缺点
优点:
1.对象只被初始化了一次,确保所有对象访问的都是唯一实例
2.由于只有一个实例的存在,节约了系统资源
缺点:
1.一个类只有一个实例,可能造成责任过重,在一定程度上违背了“单一职责”原理。
2.因为单例模式没有抽象层,因此单例类的扩展有很大的困难。
3.单例对象一旦创建,指针是保存在静态区的,单例对象的实例会在应用程序终止之后才会被释放。
3.单列的实现
我们通常写单例模式是这样的:
@interface SingleObject : NSObject
+(instancetype)shareInstance;
@end
#import "SingleObject.h"
static SingleObject *_singleInstance = nil;
@implementation SingleObject
+(instancetype)shareInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (_singleInstance == nil) {
_singleInstance = [[self alloc]init];
}
});
return _singleInstance;
}
@end
这样写看上去没有什么问题,我们来创建对象输出一下他们的内存地址
SingleObject *single1 = [SingleObject shareInstance];
SingleObject *single2 = [SingleObject shareInstance];
SingleObject *single3 = [[SingleObject alloc]init];
SingleObject *single4 = [SingleObject new];
NSLog(@"第一个对象的地址%p",single1);
NSLog(@"第二个对象的地址%p",single2);
NSLog(@"第三个对象的地址%p",single3);
NSLog(@"第四个对象的地址%p",single4);
2017-10-25 23:32:43.404920+0800 单例[1316:107887] 第一个对象的地址0x60c000006e90
2017-10-25 23:32:43.405053+0800 单例[1316:107887] 第二个对象的地址0x60c000006e90
2017-10-25 23:32:43.405165+0800 单例[1316:107887] 第三个对象的地址0x60c000006e80
2017-10-25 23:32:43.405281+0800 单例[1316:107887] 第四个对象的地址0x60c000006ea0
在这里我们可以看到对象一和对象二的内存地址是一样的,但是对象三和对象四的内存地址与对象一和对象二是各不一样的。是我们创建单例的方法错了吗?其实严格意义上来说也不是这样的,只要我们保证我们创建这个单例对象都是通过shareInstance方法来创建的就没问题。但是一个团队,开发人员习惯不一样,有时候难免会调用方法习惯不一样,那么我们上面的方法就会出问题,不能保证这是一个单例对象。
为什么会出现上面的问题呢?我们知道我们创建一个对象通常有两步操作,分别是alloc和init,alloc是为了给这个对象分配内存,init是为了初始化对象。我们上面的第三种方法通过alloc来调用,但是其实OC内部会通过+(instancetype)allocWithZone:方法来进行内存的分配,所以为了保证单例对象只会创建一个,我们必须重写+(instancetype)allocWithZone:方法。而有时候新创建的对象可能是老对象直接调用copy方法得来的,所以我们最好重写-(id)copyWithZone:(NSZone *)zone和-(id)mutableCopyWithZone:(NSZone *)zone来直接返回这个实例对象。所以改正后的方法应该是这样的:
@interface SingleObject : NSObject
+(instancetype)shareInstance;
@end
#import "SingleObject.h"
static SingleObject *_singleInstance = nil;
@implementation SingleObject
+(instancetype)shareInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (_singleInstance == nil) {
_singleInstance = [[self alloc]init];
}
});
return _singleInstance;
}
+(instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_singleInstance = [super allocWithZone:zone];
});
return _singleInstance;
}
-(id)copyWithZone:(NSZone *)zone
{
return _singleInstance;
}
-(id)mutableCopyWithZone:(NSZone *)zone {
return _singleInstance;
}
@end
此时我们来输出一下对象的内存地址:
SingleObject *single1 = [SingleObject shareInstance];
SingleObject *single2 = [SingleObject shareInstance];
SingleObject *single3 = [[SingleObject alloc]init];
SingleObject *single4 = [SingleObject new];
NSLog(@"第一个对象的地址%p",single1);
NSLog(@"第二个对象的地址%p",single2);
NSLog(@"第三个对象的地址%p",single3);
NSLog(@"第四个对象的地址%p",single4);
2017-10-25 23:36:17.818737+0800 单例[1350:112125] 第一个对象的地址0x60000001fe20
2017-10-25 23:36:17.819213+0800 单例[1350:112125] 第二个对象的地址0x60000001fe20
2017-10-25 23:36:17.820069+0800 单例[1350:112125] 第三个对象的地址0x60000001fe20
2017-10-25 23:36:17.820238+0800 单例[1350:112125] 第四个对象的地址0x60000001fe20
此时我们发现所有的对象内存地址都一样了,那么表示我们此时创建单例的方法是正确的。
但是我们需要注意的是,我们的单例可能会有属性,而有时我们需要在创建的时候就对属性进行初始化,但是在后面我们可能对这个属性的值做了更改,但是我们访问的时候希望访问的是更改后的值,而不是初始化的值,此时我们应该怎么来处理呢,我们来更改一下代码:
@interface SingleObject : NSObject
@property (nonatomic,copy)NSString *name;
+(instancetype)shareInstance;
@end
#import "SingleObject.h"
static SingleObject *_singleInstance = nil;
@implementation SingleObject
-(instancetype)init
{
self = [super init];
if (self) {
_name = @"哈哈";
}
return self;
}
+(instancetype)shareInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (_singleInstance == nil) {
_singleInstance = [[self alloc]init];
}
});
return _singleInstance;
}
+(instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_singleInstance = [super allocWithZone:zone];
});
return _singleInstance;
}
-(id)copyWithZone:(NSZone *)zone
{
return _singleInstance;
}
-(id)mutableCopyWithZone:(NSZone *)zone {
return _singleInstance;
}
@end
在这里我们给单例加了一个属性,同时重写了init方法来对属性进行初始化,此时我们来看一下代码:
[super viewDidLoad];
SingleObject *single1 = [SingleObject shareInstance];
single1.name = @"你好啊";
SingleObject *single2 = [SingleObject shareInstance];
SingleObject *single3 = [[SingleObject alloc]init];
SingleObject *single4 = [SingleObject new];
NSLog(@"第一个对象的地址%p name%@",single1,single1.name);
NSLog(@"第二个对象的地址%p name%@",single2,single2.name);
NSLog(@"第三个对象的地址%p name%@",single3,single3.name);
NSLog(@"第四个对象的地址%p name%@",single4,single4.name);
2017-10-25 23:46:51.838957+0800 单例[1410:122563] 第一个对象的地址0x604000010a70 name哈哈
2017-10-25 23:46:51.839120+0800 单例[1410:122563] 第二个对象的地址0x604000010a70 name哈哈
2017-10-25 23:46:51.839235+0800 单例[1410:122563] 第三个对象的地址0x604000010a70 name哈哈
2017-10-25 23:46:51.839338+0800 单例[1410:122563] 第四个对象的地址0x604000010a70 name哈哈
这个时候我们发现不对了,我们在第一个对象里面分明对属性做了更改,为什么我们输出的属性结果不是我们更改的结果呢?这不是一个单例吗?为什么属性没有变化呢?因为在我们上面的代码里面我们重写了init方法,当只创建对象一的时候我们的属性结果确实变为了你好啊,但是后面我们创建对象三的时候我们调用了init方法,此时对象又被重新重置为了哈哈,而且由于对象是单例,所以所有对象的属性全部跟着变化了。
那么我们应该怎么处理呢,我们的处理有两种办法,第一种是把对象的初始化放到shareInstance方法里面去,保证它只执行一次,还有就是重写init方法的时候用dispatch_once,保证只被初始化一次。下面是两种方法的实现:
第一种
+(instancetype)shareInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (_singleInstance == nil) {
_singleInstance = [[self alloc]init];
_singleInstance.name = @"哈哈";
}
});
return _singleInstance;
}
+(instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_singleInstance = [super allocWithZone:zone];
});
return _singleInstance;
}
-(id)copyWithZone:(NSZone *)zone
{
return _singleInstance;
}
-(id)mutableCopyWithZone:(NSZone *)zone {
return _singleInstance;
}
第二种:
static SingleObject *_singleInstance = nil;
@implementation SingleObject
-(instancetype)init
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_name = @"哈哈";
});
return _singleInstance;
}
+(instancetype)shareInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (_singleInstance == nil) {
_singleInstance = [[self alloc]init];
}
});
return _singleInstance;
}
+(instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_singleInstance = [super allocWithZone:zone];
});
return _singleInstance;
}
-(id)copyWithZone:(NSZone *)zone
{
return _singleInstance;
}
-(id)mutableCopyWithZone:(NSZone *)zone {
return _singleInstance;
}
其实单例还有一种写法,这种方式使用加锁的方式来实现的:
+(instancetype)shareInstance
{
@synchronized(self){
if (_singleInstance == nil) {
_singleInstance = [[SingleObject alloc]init];
}
}
return _singleInstance;
}
+(instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self){
if (_singleInstance == nil) {
_singleInstance = [super allocWithZone:zone];
}
}
return _singleInstance;
}
-(id)copyWithZone:(NSZone *)zone
{
return _singleInstance;
}
-(id)mutableCopyWithZone:(NSZone *)zone {
return _singleInstance;
}
在这里我们为什么要加锁呢,因为当我们再多线程环境下时,如果线程一创建实例的时候发现实例为nil,他去创建实例,但是线程二此时也去创建实例,由于线程一创建实例可能还没有创建完毕,于是线程二也走创建实例的代码,这样就会创建两个实例对象,此时不符合我们的单例的要求,所以我们要给所有的涉及到内存分配以及创建实例的地方加上锁,防止实例被创建多个。
所以我们在使用单例的时候一定要注意正确的打开方式。