單例模式可能是設計模式中最簡單的形式了日矫,這一模式的意圖就是使得類中的一個對象成為系統(tǒng)中的唯一實例或辖。它提供了對類的對象所提供的資源的全局訪問點者填。因此需要用一種只允許生成對象類的唯一實例的機制刻伊。
下面讓我們來看下單例的作用:
- 可以保證的程序運行過程训堆,一個類只有一個示例描验,而且該實例易于供外界訪問
- 從而方便地控制了實例個數(shù),并節(jié)約系統(tǒng)資源坑鱼。
方法一(誤)
+ (instancetype)sharedInstance
{
static Singleton *instance = nil;
if (!instance) {
instance = [[Singleton alloc] init];
}
return instance;
}
這種方式的單例不是線程安全的膘流。
假設此時有兩條線程:線程1和線程2,都在調(diào)用shareInstance方法來創(chuàng)建單例鲁沥,那么線程1運行到if (instance == nil)出發(fā)現(xiàn)instance = nil,那么就會初始化一個instance呼股,假設此時線程2也運行到if的判斷處了,此時線程1還沒有創(chuàng)建完成實例instance画恰,所以此時instance = nil還是成立的彭谁,那么線程2又會創(chuàng)建一個instace。
為了解決線程安全問題允扇,可以使用dispatch_once缠局、互斥鎖则奥。
方法二 (誤)
static Singleton *instance = nil;
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[Singleton alloc] init];
});
return instance;
}
或
static Singleton *instance = nil;
+ (instancetype)sharedInstance
{
@synchronized (self) {
if (!instance) {
instance = [[Singleton alloc] init];
}
}
return instance;
}
上面的兩個方法保證了線程安全,但是不夠全面甩鳄。如果使用其他方式創(chuàng)建逞度,能創(chuàng)建出不同的對象额划,違背了單例的設計原則妙啃。
Singleton *s = nil;
s = [Singleton sharedInstance];
NSLog(@"%@", s);
s = [[Singleton alloc] init];
NSLog(@"%@", s);
s = [Singleton new];
NSLog(@"%@", s);
打印出三個不同的地址
2016-12-21 20:46:30.414 Singleton[28843:2198096] <Singleton: 0x6000000168c0>
2016-12-21 20:46:30.415 Singleton[28843:2198096] <Singleton: 0x610000016340>
2016-12-21 20:46:30.415 Singleton[28843:2198096] <Singleton: 0x6180000164a0>
方法三(誤)
為了防止別人不小心利用alloc/init方式創(chuàng)建示例,也為了防止別人故意為之俊戳,我們要保證不管用什么方式創(chuàng)建都只能是同一個實例對象揖赴,這就得重寫另一個方法。
在方法二的基礎上增加重寫下面的方法:
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [super allocWithZone:zone];
});
return instance;
}
再測試抑胎,發(fā)現(xiàn)打印出來的地址都一樣了燥滑。
但是,還沒結(jié)束阿逃。
我們添加一些屬性铭拧,并在-init方法中進行初始化:
@property (assign, nonatomic)int height;
@property (strong, nonatomic)NSObject *object;
@property (strong, nonatomic)NSMutableArray *array;
然后重寫-description方法:
- (NSString *)description
{
NSString *result = @"";
result = [result stringByAppendingFormat:@"<%@: %p>",[self class], self];
result = [result stringByAppendingFormat:@" height = %d,",self.height];
result = [result stringByAppendingFormat:@" array = %p,",self.array];
result = [result stringByAppendingFormat:@" object = %p,",self.object];
return result;
}
還用上面的方法,打印結(jié)果:
2016-12-21 20:58:03.523 Singleton[29239:2252268] <Singleton: 0x608000039d00> height = 10, arrayM = 0x60800005b150, object = 0x60800000b3e0,
2016-12-21 20:58:03.523 Singleton[29239:2252268] <Singleton: 0x608000039d00> height = 10, arrayM = 0x618000052540, object = 0x61800000b430,
2016-12-21 20:58:03.524 Singleton[29239:2252268] <Singleton: 0x608000039d00> height = 10, arrayM = 0x60800004ae00, object = 0x60800000b3e0,
可以看到恃锉,盡管使用的是同一個示例搀菩,可是他們的屬性卻不一樣。
因為盡管沒有為示例重新分配內(nèi)存空間破托,但是因為又執(zhí)行了init方法肪跋,會導致property被重新初始化。
方法四
為了保證屬性的初始化只執(zhí)行一次土砂,可以將屬性的初始化或者默認值設置也限制只執(zhí)行一次州既。我們這里加上dispatch_once。
+ (instancetype)sharedInstance
{
return [[Singleton alloc] init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [super allocWithZone:zone];
});
return instance;
}
- (instancetype)init
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [super init];
if (instance) {
instance.height = 10;
instance.object = [[NSObject alloc] init];
instance.array = [[NSMutableArray alloc] init];
}
});
return instance;
}
這種方式保證了單例的唯一萝映,也保證了屬性初始化的唯一吴叶。
關于線程安全
GCD的dispatch_once方式:保證程序在運行過程中只會被運行一次,那么假設此時線程1先執(zhí)行shareInstance方法序臂,創(chuàng)建了一個實例對象晤郑,線程2就不會再去執(zhí)行dispatch_once的代碼了。從而保證了只會創(chuàng)建一個實例對象贸宏。
互斥鎖方式:會把鎖內(nèi)的代碼當做一個任務造寝,這個任務執(zhí)行完畢前,不會被其他線程訪問吭练。
但是這種簡單的互斥鎖方式在每次調(diào)用單例時都會鎖一次诫龙,很影響性能,單例使用越頻繁鲫咽,影響越大签赃。
優(yōu)化互斥鎖方式
DCL(double check lock):雙重檢查模式是優(yōu)化了的互斥鎖方式谷异,過程就是check-lock-check,是對靜態(tài)變量instance的兩次判空锦聊。第一次判空避免了不必要的同步歹嘹,第二次判空是為了創(chuàng)建實例。
將上面的簡單互斥鎖方式修改一下:
if (!instance) {
@synchronized (self) {
if (!instance) {
instance = [super allocWithZone:zone];
}
}
}
return instance;
DCL優(yōu)點是資源利用率高孔庭,第一次執(zhí)行時單例對象才被實例化尺上,效率高。缺點是第一次加載時反應稍慢一些圆到,在高并發(fā)環(huán)境下也有一定的缺陷怎抛,雖然發(fā)生的概率很小。
效率:
GCD > DCL > 簡單互斥鎖
使用+load或+initialize
load方法與initialize方法都會被Runtime自動調(diào)用一次芽淡,并且在Runtime情況下马绝,這兩個方法都是線程安全的。
根據(jù)這種特性挣菲,來實現(xiàn)單例類富稻。
+ (void)initialize
{
if ([self class] == [Singleton class] && instance == nil) {
instance = [[Singleton alloc] init];
instance.height = 10;
instance.object = [[NSObject alloc] init];
instance.array = [[NSMutableArray alloc] init];
}
}
+ (instancetype)sharedInstance
{
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
if (instance == nil) {
instance = [super allocWithZone:zone];
}
return instance;
}
-
if([self class] == [Singleton class]...)
是為了保證 initialize方法只有在本類而非subclass時才執(zhí)行單例初始化方法。 -
if (... && instance == nil)
是為了防止+initialize多次調(diào)用而產(chǎn)生多個實例(除了Runtime調(diào)用白胀,我們也可以顯示調(diào)用+initialize方法)椭赋。經(jīng)過測試,當我們將+initialize方法本身作為class的第一個方法執(zhí)行時纹笼,Runtime的+initialize會被先調(diào)用(這保證了線程安全)纹份,然后我們自己顯示調(diào)用的+initialize函數(shù)再被調(diào)用。 由于+initialize方法的第一次調(diào)用一定是Runtime調(diào)用廷痘,而Runtime又保證了線程安全性蔓涧,因此這里只簡單的檢測 singalObject == nil即可。
最好不用+load來做單例是因為它是在程序被裝載時調(diào)用的笋额,可能單例所依賴的環(huán)境尚未形成元暴,它比較適合對Class做設置。(更多關于+load和+initialize的知識兄猩,看這里)
使用宏
如果我們需要在程序中創(chuàng)建多個單例茉盏,那么需要在每個類中都寫上一次上述代碼,非常繁瑣枢冤。
我們可以使用宏來封裝單例的創(chuàng)建鸠姨,這樣任何類需要創(chuàng)建單例,只需要一行代碼就搞定了淹真。
#define SingletonH(name) + (instancetype)shared##name;
#define SingletonM(name) \
static id instance = nil; \
+ (instancetype)sharedInstance \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
instance = [[[self class] alloc] init]; \
}); \
return instance; \
} \
其他
當然單例如果實現(xiàn)了NSCopying和NSMutableCopying協(xié)議讶迁,可以補充下面的方法:
- (id)copyWithZone:(NSZone *)zone
{
return instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return instance;
}
結(jié)束語
有關iOS設計模式的全部示例在這里examples