在開發中經常會用到單例設計模式,目的就是為了在程式的整個生命週期內,只會建立一個類的例項物件,而且只要程式不被殺死,該例項物件就不會被釋放。下面我們來看看單例的概念、用途、如何建立,以便加深理解。
定義
保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。
作用
- 在應用這個模式時,單例物件的類必須保證只有一個例項存在。許多時候整個系統只需要擁有一個的全域性物件,這樣有利於我們協調系統整體的行為。比如在APP開發中我們可能在任何地方都要使用使用者的資訊,那麼可以在登入的時候就把使用者資訊存放在一個檔案裡面,這些配置資料由一個單例物件統一讀取,然後服務程式中的其他物件再通過這個單例物件獲取這些配置資訊。這種方式簡化了在複雜環境下的配置管理。
- 有的情況下,某個類可能只能有一個例項。比如說你寫了一個類用來播放音樂,那麼不管任何時候只能有一個該類的例項來播放聲音。再比如,一臺計算機上可以連好幾個印表機,但是這個計算機上的列印程式只能有一個,這裡就可以通過單例模式來避免兩個列印任務同時輸出到印表機中,即在整個的列印過程中我只有一個列印程式的例項。
建立單例
有兩種方法來建立單例,下面分別介紹
1、GCD方式建立單例
static id _instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
}
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return _instance;
}複製程式碼
2、互斥鎖方式
static id _instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (_instance == nil) {
_instance = [super allocWithZone:zone];
}
}
return _instance;
}
+ (instancetype)sharedInstance
{
@synchronized(self) {
if (_instance == nil) {
_instance = [[self alloc] init];
}
}
return _instance;
}
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}複製程式碼
上面兩種方式都可以建立單例,而且保證了使用者不管是通過shareInstance方法,還是alloc、copy方法得到的例項都是一樣的。
上面程式碼的關鍵之處就在於如何在多執行緒情況下保證建立的單例還是同一個。
我們先看看在GCD情況下,如果不使用dispatch_once和同步鎖建立單例會出現什麼問題,去掉兩者後建立單例的程式碼如下
+ (instancetype)sharedInstance
{
if (_instance == nil) {
_instance = [[self alloc] init];
}
}複製程式碼
假設此時有兩條執行緒:執行緒1和執行緒2,都在呼叫shareInstance方法來建立單例,那麼執行緒1執行到if (_instance == nil)
出發現_instance = nil,那麼就會初始化一個_instance,假設此時執行緒2也執行到if的判斷處了,此時執行緒1還沒有建立完成例項_instance,所以此時_instance = nil還是成立的,那麼執行緒2又會建立一個_instace。
此時就建立了兩個例項物件,導致問題。
解決辦法1、使用dispatch_once
dispatch_once保證程式在執行過程中只會被執行一次,那麼假設此時執行緒1先執行shareInstance方法,建立了一個例項物件,執行緒2就不會再去執行dispatch_once的程式碼了。從而保證了只會建立一個例項物件。
解決辦法2、使用互斥鎖
假設此時執行緒1在執行shareInstance方法,那麼synchronize大括號內建立單例的程式碼,如下所示:
if (_instance == nil) {
_instance = [[self alloc] init];
}複製程式碼
就會被當做一個任務被加上了一把鎖。此時假設執行緒2也想執行shareInstance方法建立單例,但是看到了執行緒1加的互斥鎖,就會進入睡眠模式。等到執行緒1執行完畢,才會被喚醒,然後去執行上面所示的建立單例的程式碼,但是此時_instance !=nil,所以不會再建立新的例項物件了。從而保證只會建立一個例項物件。
但是互斥鎖會影響效能,所以最好還是使用GCD方式建立單例。
巨集建立單例
如果我們需要在程式中建立多個單例,那麼需要在每個類中都寫上一次上述程式碼,非常繁瑣。
我們可以使用巨集來封裝單例的建立,這樣任何類需要建立單例,只需要一行程式碼就搞定了。
實現程式碼
Singleton.h檔案
==================================
#define SingletonH(name) + (instancetype)shared##name;
#define SingletonM(name) \
static id _instance; \
\
+ (instancetype)allocWithZone:(struct _NSZone *)zone \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [super allocWithZone:zone]; \
}); \
return _instance; \
} \
\
+ (instancetype)shared##name \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [[self alloc] init]; \
}); \
return _instance; \
} \
\
- (id)copyWithZone:(NSZone *)zone \
{ \
return _instance; \
}\
\
- (id)mutableCopyWithZone:(NSZone *)zone { \
return _instance; \
}複製程式碼
如何呼叫
假設我們要在類viewcontroller中使用,呼叫方法如下:
viewcontroller.h檔案
===========================
#import <UIKit/UIKit.h>
#import "Singleton.h"
@interface ViewController : UIViewController
SingletonH(viewController)
@end
viewcontroller.m檔案
===========================
@interface ViewController ()
@end
@implementation ViewController
SingletonM(ViewController)
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@ %@ %@ %@", [ViewController sharedViewController],[ViewController sharedViewController], [[ViewController alloc] init],[[ViewController alloc] init]);
}
@end複製程式碼
輸出結果
<ViewController: 0x7f897061bc00> <ViewController: 0x7f897061bc00> <ViewController: 0x7f897061bc00> <ViewController: 0x7f897061bc00>複製程式碼
可以看到四個物件的記憶體地址完全一樣,說明是同一個物件