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,他去創建實例,但是線程二此時也去創建實例,由于線程一創建實例可能還沒有創建完畢,于是線程二也走創建實例的代碼,這樣就會創建兩個實例對象,此時不符合我們的單例的要求,所以我們要給所有的涉及到內存分配以及創建實例的地方加上鎖,防止實例被創建多個。
所以我們在使用單例的時候一定要注意正確的打開方式。