本文轉(zhuǎn)載http://www.cocoachina.com/ios/20171123/21300.html
單例模式大概是設(shè)計模式中最簡單的一個习寸。本來沒什么好說的棺禾,但是實踐過程中還是有一些坑桦山。所以本文小結(jié)一下在iOS開發(fā)中的單例模式螺戳。
一、 什么是單例模式
按照四人幫(GOF)教科書的說法碌燕,標(biāo)準(zhǔn)定義是這樣的:
Ensures a ``class
has only one instance, and provide a global point of access to it.
保證一個類只有一個實例误证,并且提供一個全局的訪問入口訪問這個實例。
然后修壕,類圖是這個樣子的:
什么時候選擇單例模式呢愈捅?
一個類必須只有一個對象〈瑞客戶端必須通過一個眾所周知的入口訪問這個對象蓝谨。
這個唯一的對象需要擴展的時候,只能通過子類化的方式青团∑┪祝客戶端的代碼能夠不需要任何修改就能夠使用擴展后的對象。
上面的官方說法壶冒,聽起來一頭霧水缕题。我的理解是這樣的。
在建模的時候胖腾,如果這個東西確實只需要一個對象,多余的對象都是無意義的瘪松,那么就考慮用單例模式咸作。比如定位管理(CLLocationManager),硬件設(shè)備就只有一個宵睦,弄再多的邏輯對象意義不大记罚。所以就會考慮用單例
二、 如何實現(xiàn)基本的單例模式壳嚎?
那么桐智,我們就用Objective-C來實現(xiàn)一下單例模式吧。
要實現(xiàn)比較好的訪問烟馅,我們就會想到用工廠方法創(chuàng)建對象说庭,提供統(tǒng)一的創(chuàng)建方法的地方給外部使用。要實現(xiàn)僅有一個對象郑趁,就會想到用一個全局的東西保存這個對象刊驴,然后在創(chuàng)建對象的工廠方法中判斷一下,如果對象存在,那么就返回該對象捆憎。如果不存在舅柜,就造一個返回出去。
于是躲惰,基本的單例實現(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;
}
看起來不錯致份。不過這個全局的變量 g_instance_dj_singleton有個缺點,就是外面的人隨便可以改础拨,為了隔離外部修改氮块,可以設(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;
}
單例的核心思想算是實現(xiàn)了太伊。
三雇锡、 多線程怎么辦?
雖然核心思想實現(xiàn)了僚焦,但是依舊不完美锰提。考慮下多線程的情況芳悲。即多個線程同時訪問這個工廠方法立肘,能夠總是保證只創(chuàng)建一個實例對象么?
顯然上面的方式是有問題的名扛。比如第一個線程執(zhí)行到第4行但是還沒有進(jìn)行賦值操作谅年,第二個線程執(zhí)行第三行。此時判斷對象依舊為nil肮韧,第二個線程也能往下執(zhí)行到創(chuàng)建對象操作的第4行融蹂。從而創(chuàng)建了多個對象。
那么弄企,如何保證多線程下依舊能夠只創(chuàng)建一個呢超燃?這里面的核心思路,是要保證s_instance_dj_singleton這個臨界資源的訪問(讀取和賦值)拘领。
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;
}
看起來多線程沒啥問題了了。不過我們可以做的更好圣猎。OC的內(nèi)部機制里有一種更加高效的方式士葫,那就是dispatch_once。性能相差好幾倍样漆,好幾十倍为障。關(guān)于性能的比對,大神們做過實驗和分析。請參考這里鳍怨。
于是呻右,我們的單例變成了這個樣子:
+ (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的坑
看起來很完美了鞋喇∩模可是Objective-C畢竟是Objective-C。別的語言侦香,諸如C++落塑,java,構(gòu)造方法可以隱藏罐韩。Objective-C中的方法憾赁,實際上都是公開的,雖然我們提供了一個方便的工廠方法的訪問入口散吵,但是里面的alloc方法依舊是可見的龙考,可以調(diào)用到的。也就是說矾睦,雖然你給了我一個工廠方法晦款,調(diào)皮的小伙伴可能依舊會使用alloc的方式創(chuàng)建對象。這樣會導(dǎo)致外面使用的時候枚冗,依舊可能創(chuàng)建多個實例缓溅。
關(guān)于這個事情的處理,可以分為兩派赁温。一個是冷酷派坛怪,技術(shù)上實現(xiàn)無論你怎么調(diào)用,我都給你同一個單例對象股囊;一個是溫柔派酝陈,是從編譯器上給調(diào)皮的小伙伴提示,你不能這么造對象毁涉,溫柔的指出有問題,但不強制約束锈死。
1. 冷酷派的實現(xiàn)
冷酷派的實現(xiàn)從OC的對象創(chuàng)建角度出發(fā)贫堰,就是把創(chuàng)建對象的各種入口給封死了。alloc待牵,copy等等其屏,無論是采用哪種方式創(chuàng)建,我都保證給出的對象是同一個缨该。
由Objective-C的一些特性可以知道偎行,在對象創(chuàng)建的時候,無論是alloc還是new,都會調(diào)用到 allocWithZone方法蛤袒。在通過拷貝的時候創(chuàng)建對象時熄云,會調(diào)用到-(id)copyWithZone:(NSZone *)zone,-(id)mutableCopyWithZone:(NSZone *)zone方法妙真。因此缴允,可以重寫這些方法,讓創(chuàng)建的對象唯一珍德。
+(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. 溫柔派的實現(xiàn)
溫柔派就直接告訴外面练般,alloc,new锈候,copy薄料,mutableCopy方法不可以直接調(diào)用。否則編譯不過泵琳。
+(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")));
我個人的話比較喜歡采用溫柔派的實現(xiàn)摄职。不需要這么多復(fù)雜的實現(xiàn)。也讓使用方有比較明確的概念這個是個單例虑稼,不要調(diào)皮琳钉。對于一般的業(yè)務(wù)場景是足夠了的。
五蛛倦、 單例模式潛在的問題
1. 內(nèi)存問題
單例模式實際上延長了對象的生命周期歌懒。那么就存在內(nèi)存問題。因為這個對象在程序的整個生命都存在溯壶。所以當(dāng)這個單例比較大的時候及皂,總是hold住那么多內(nèi)存,就需要考慮這件事了且改。另外验烧,可能單例本身并不大,但是它如果強引用了另外的比較大的對象又跛,也算是一個問題碍拆。別的對象因為單例對象不釋放而不釋放。
當(dāng)然這個問題也有一定的辦法慨蓝。比如對于一些可以重新加載的對象感混,在需要的時候加載,用完之后礼烈,單例對象就不再強引用弧满,從而把原先hold住的對象釋放掉。下次需要再加載回來此熬。
2. 循環(huán)依賴問題
在開發(fā)過程中庭呜,單例對象可能有一些屬性滑进,一般會放在init的時候創(chuàng)建和初始化。這樣募谎,比如如果單例A的m屬性依賴于單例B扶关,單例B的屬性n依賴于單例A,初始化的時候就會出現(xiàn)死循環(huán)依賴近哟。死在dispatch_once里驮审。