本文轉(zhuǎn)載http://www.cocoachina.com/ios/20171123/21300.html
單例模式大概是設(shè)計(jì)模式中最簡(jiǎn)單的一個(gè)。本來(lái)沒(méi)什么好說(shuō)的,但是實(shí)踐過(guò)程中還是有一些坑。所以本文小結(jié)一下在iOS開(kāi)發(fā)中的單例模式。
一、 什么是單例模式
按照四人幫(GOF)教科書(shū)的說(shuō)法,標(biāo)準(zhǔn)定義是這樣的:
Ensures a ``class has only one instance, and provide a global point of access to it.
保證一個(gè)類只有一個(gè)實(shí)例,并且提供一個(gè)全局的訪問(wèn)入口訪問(wèn)這個(gè)實(shí)例。
然后,類圖是這個(gè)樣子的:

什么時(shí)候選擇單例模式呢?
一個(gè)類必須只有一個(gè)對(duì)象??蛻舳吮仨毻ㄟ^(guò)一個(gè)眾所周知的入口訪問(wèn)這個(gè)對(duì)象。
這個(gè)唯一的對(duì)象需要擴(kuò)展的時(shí)候,只能通過(guò)子類化的方式??蛻舳说拇a能夠不需要任何修改就能夠使用擴(kuò)展后的對(duì)象。
上面的官方說(shuō)法,聽(tīng)起來(lái)一頭霧水。我的理解是這樣的。
在建模的時(shí)候,如果這個(gè)東西確實(shí)只需要一個(gè)對(duì)象,多余的對(duì)象都是無(wú)意義的,那么就考慮用單例模式。比如定位管理(CLLocationManager),硬件設(shè)備就只有一個(gè),弄再多的邏輯對(duì)象意義不大。所以就會(huì)考慮用單例
二、 如何實(shí)現(xiàn)基本的單例模式?
那么,我們就用Objective-C來(lái)實(shí)現(xiàn)一下單例模式吧。
要實(shí)現(xiàn)比較好的訪問(wèn),我們就會(huì)想到用工廠方法創(chuàng)建對(duì)象,提供統(tǒng)一的創(chuàng)建方法的地方給外部使用。要實(shí)現(xiàn)僅有一個(gè)對(duì)象,就會(huì)想到用一個(gè)全局的東西保存這個(gè)對(duì)象,然后在創(chuàng)建對(duì)象的工廠方法中判斷一下,如果對(duì)象存在,那么就返回該對(duì)象。如果不存在,就造一個(gè)返回出去。
于是,基本的單例實(shí)現(xiàn)就這樣了:
DJSingleton * g_instance_dj_singleton = nil ;
+ (DJSingleton *)shareInstance{
if(g_instance_dj_singleton == nil) {
g_instance_dj_singleton = [[DJSingleton alloc] init];
}
return` `(DJSingleton *)g_instance_dj_singleton;
}
看起來(lái)不錯(cuò)。不過(guò)這個(gè)全局的變量 g_instance_dj_singleton有個(gè)缺點(diǎn),就是外面的人隨便可以改,為了隔離外部修改,可以設(shè)置成靜態(tài)變量,就是這樣子:
+ (DJSingleton *)shareInstance{
static DJSingleton * s_instance_dj_singleton = nil ;
if (s_instance_dj_singleton == nil) {
s_instance_dj_singleton = [[DJSingleton alloc] init];
}
return (DJSingleton *)s_instance_dj_singleton;
}
單例的核心思想算是實(shí)現(xiàn)了。
三、 多線程怎么辦?
雖然核心思想實(shí)現(xiàn)了,但是依舊不完美。考慮下多線程的情況。即多個(gè)線程同時(shí)訪問(wèn)這個(gè)工廠方法,能夠總是保證只創(chuàng)建一個(gè)實(shí)例對(duì)象么?
顯然上面的方式是有問(wèn)題的。比如第一個(gè)線程執(zhí)行到第4行但是還沒(méi)有進(jìn)行賦值操作,第二個(gè)線程執(zhí)行第三行。此時(shí)判斷對(duì)象依舊為nil,第二個(gè)線程也能往下執(zhí)行到創(chuàng)建對(duì)象操作的第4行。從而創(chuàng)建了多個(gè)對(duì)象。
那么,如何保證多線程下依舊能夠只創(chuàng)建一個(gè)呢?這里面的核心思路,是要保證s_instance_dj_singleton這個(gè)臨界資源的訪問(wèn)(讀取和賦值)。
iOS下控制多線程的方式有很多,可以使用NSLock,可以@synchronized等各種線程同步的技術(shù)。于是,我們的單例代碼變成了這樣:
+ (DJSingleton *)shareInstance{
static DJSingleton * s_instance_dj_singleton = nil ;
@synchronized(self) {
if (s_instance_dj_singleton == nil) {
s_instance_dj_singleton = [[DJSingleton alloc] init];
}
}
return (DJSingleton *)s_instance_dj_singleton;
}
看起來(lái)多線程沒(méi)啥問(wèn)題了了。不過(guò)我們可以做的更好。OC的內(nèi)部機(jī)制里有一種更加高效的方式,那就是dispatch_once。性能相差好幾倍,好幾十倍。關(guān)于性能的比對(duì),大神們做過(guò)實(shí)驗(yàn)和分析。請(qǐng)參考這里。
于是,我們的單例變成了這個(gè)樣子:
+ (DJSingleton *)shareInstance{
static DJSingleton * s_instance_dj_singleton = nil ;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (s_instance_dj_manager == nil) {
s_instance_dj_manager = [[DJSingleton alloc] init];
}
});
return (DJSingleton *)s_instance_dj_singleton;
}
四、Objective-C的坑
看起來(lái)很完美了??墒荗bjective-C畢竟是Objective-C。別的語(yǔ)言,諸如C++,java,構(gòu)造方法可以隱藏。Objective-C中的方法,實(shí)際上都是公開(kāi)的,雖然我們提供了一個(gè)方便的工廠方法的訪問(wèn)入口,但是里面的alloc方法依舊是可見(jiàn)的,可以調(diào)用到的。也就是說(shuō),雖然你給了我一個(gè)工廠方法,調(diào)皮的小伙伴可能依舊會(huì)使用alloc的方式創(chuàng)建對(duì)象。這樣會(huì)導(dǎo)致外面使用的時(shí)候,依舊可能創(chuàng)建多個(gè)實(shí)例。
關(guān)于這個(gè)事情的處理,可以分為兩派。一個(gè)是冷酷派,技術(shù)上實(shí)現(xiàn)無(wú)論你怎么調(diào)用,我都給你同一個(gè)單例對(duì)象;一個(gè)是溫柔派,是從編譯器上給調(diào)皮的小伙伴提示,你不能這么造對(duì)象,溫柔的指出有問(wèn)題,但不強(qiáng)制約束。
1. 冷酷派的實(shí)現(xiàn)
冷酷派的實(shí)現(xiàn)從OC的對(duì)象創(chuàng)建角度出發(fā),就是把創(chuàng)建對(duì)象的各種入口給封死了。alloc,copy等等,無(wú)論是采用哪種方式創(chuàng)建,我都保證給出的對(duì)象是同一個(gè)。
由Objective-C的一些特性可以知道,在對(duì)象創(chuàng)建的時(shí)候,無(wú)論是alloc還是new,都會(huì)調(diào)用到 allocWithZone方法。在通過(guò)拷貝的時(shí)候創(chuàng)建對(duì)象時(shí),會(huì)調(diào)用到-(id)copyWithZone:(NSZone *)zone,-(id)mutableCopyWithZone:(NSZone *)zone方法。因此,可以重寫(xiě)這些方法,讓創(chuàng)建的對(duì)象唯一。
+(id)allocWithZone:(NSZone *)zone{
return [DJSingleton sharedInstance];
}
+(DJSingleton *) sharedInstance{
static DJSingleton * s_instance_dj_singleton = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
s_instance_dj_singleton = [[super allocWithZone:nil] init];
});
return s_instance_dj_singleton;
}
-(id)copyWithZone:(NSZone *)zone{
return [DJSingleton sharedInstance];
}
-(id)mutableCopyWithZone:(NSZone *)zone{
return [DJSingleton sharedInstance];
}
2. 溫柔派的實(shí)現(xiàn)
溫柔派就直接告訴外面,alloc,new,copy,mutableCopy方法不可以直接調(diào)用。否則編譯不過(guò)。
+(instancetype) alloc __attribute__((unavailable("call sharedInstance instead")));
+(instancetype) new __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) copy __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));
我個(gè)人的話比較喜歡采用溫柔派的實(shí)現(xiàn)。不需要這么多復(fù)雜的實(shí)現(xiàn)。也讓使用方有比較明確的概念這個(gè)是個(gè)單例,不要調(diào)皮。對(duì)于一般的業(yè)務(wù)場(chǎng)景是足夠了的。
五、 單例模式潛在的問(wèn)題
1. 內(nèi)存問(wèn)題
單例模式實(shí)際上延長(zhǎng)了對(duì)象的生命周期。那么就存在內(nèi)存問(wèn)題。因?yàn)檫@個(gè)對(duì)象在程序的整個(gè)生命都存在。所以當(dāng)這個(gè)單例比較大的時(shí)候,總是hold住那么多內(nèi)存,就需要考慮這件事了。另外,可能單例本身并不大,但是它如果強(qiáng)引用了另外的比較大的對(duì)象,也算是一個(gè)問(wèn)題。別的對(duì)象因?yàn)閱卫龑?duì)象不釋放而不釋放。
當(dāng)然這個(gè)問(wèn)題也有一定的辦法。比如對(duì)于一些可以重新加載的對(duì)象,在需要的時(shí)候加載,用完之后,單例對(duì)象就不再?gòu)?qiáng)引用,從而把原先hold住的對(duì)象釋放掉。下次需要再加載回來(lái)。
2. 循環(huán)依賴問(wèn)題
在開(kāi)發(fā)過(guò)程中,單例對(duì)象可能有一些屬性,一般會(huì)放在init的時(shí)候創(chuàng)建和初始化。這樣,比如如果單例A的m屬性依賴于單例B,單例B的屬性n依賴于單例A,初始化的時(shí)候就會(huì)出現(xiàn)死循環(huán)依賴。死在dispatch_once里。