iOS中的單例模式

單例模式大概是設計模式中最簡單的一個。本來沒什么好說的,但是實踐過程中還是有一些坑。所以本文小結一下在iOS開發中的單例模式。

一、 什么是單例模式

按照四人幫(GOF)教科書的說法,標準定義是這樣的:

Ensures a class has only one instance, and provide a global point of access to it.

保證一個類只有一個實例,并且提供一個全局的訪問入口訪問這個實例。
然后,類圖是這個樣子的:


單例類圖

什么時候選擇單例模式呢?

  • 一個類必須只有一個對象。客戶端必須通過一個眾所周知的入口訪問這個對象。
  • 這個唯一的對象需要擴展的時候,只能通過子類化的方式。客戶端的代碼能夠不需要任何修改就能夠使用擴展后的對象。

上面的官方說法,聽起來一頭霧水。我的理解是這樣的。
在建模的時候,如果這個東西確實只需要一個對象,多余的對象都是無意義的,那么就考慮用單例模式。比如定位管理(CLLocationManager),硬件設備就只有一個,弄再多的邏輯對象意義不大。所以就會考慮用單例。

二、 如何實現基本的單例模式?

那么,我們就用Objective-C來實現一下單例模式吧。
要實現比較好的訪問,我們就會想到用工廠方法創建對象,提供統一的創建方法的地方給外部使用。要實現僅有一個對象,就會想到用一個全局的東西保存這個對象,然后在創建對象的工廠方法中判斷一下,如果對象存在,那么就返回該對象。如果不存在,就造一個返回出去。
于是,基本的單例實現就這樣了:

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有個缺點,就是外面的人隨便可以改,為了隔離外部修改,可以設置成靜態變量,就是這樣子:

1 + (DJSingleton *)shareInstance{
2         static DJSingleton * s_instance_dj_singleton = nil ;
3         if (s_instance_dj_singleton == nil) {
4             s_instance_dj_singleton = [[DJSingleton alloc] init];
5         }
6     return (DJSingleton *)s_instance_dj_singleton;
7 }

單例的核心思想算是實現了。

三、 多線程怎么辦?

雖然核心思想實現了,但是依舊不完美。考慮下多線程的情況。即多個線程同時訪問這個工廠方法,能夠總是保證只創建一個實例對象么?
顯然上面的方式是有問題的。比如第一個線程執行到第4行但是還沒有進行賦值操作,第二個線程執行第三行。此時判斷對象依舊為nil,第二個線程也能往下執行到創建對象操作的第4行。從而創建了多個對象。
那么,如何保證多線程下依舊能夠只創建一個呢?這里面的核心思路,是要保證s_instance_dj_singleton這個臨界資源的訪問(讀取和賦值)。
iOS下控制多線程的方式有很多,可以使用NSLock,可以@synchronized等各種線程同步的技術。于是,我們的單例代碼變成了這樣:

1 + (DJSingleton *)shareInstance{
2         static DJSingleton * s_instance_dj_singleton = nil ;
3           @synchronized(self) {
4                if (s_instance_dj_singleton == nil) {
5                   s_instance_dj_singleton = [[DJSingleton alloc] init];
6               }
7            }
8         return (DJSingleton *)s_instance_dj_singleton;
9 }

看起來多線程沒啥問題了了。不過我們可以做的更好。OC的內部機制里有一種更加高效的方式,那就是dispatch_once。性能相差好幾倍,好幾十倍。關于性能的比對,大神們做過實驗和分析。請參考這里
于是,我們的單例變成了這個樣子:

+ (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,構造方法可以隱藏。Objective-C中的方法,實際上都是公開的,雖然我們提供了一個方便的工廠方法的訪問入口,但是里面的alloc方法依舊是可見的,可以調用到的。也就是說,雖然你給了我一個工廠方法,調皮的小伙伴可能依舊會使用alloc的方式創建對象。這樣會導致外面使用的時候,依舊可能創建多個實例。
關于這個事情的處理,可以分為兩派。一個是冷酷派,技術上實現無論你怎么調用,我都給你同一個單例對象;一個是溫柔派,是從編譯器上給調皮的小伙伴提示,你不能這么造對象,溫柔的指出有問題,但不強制約束。

1. 冷酷派的實現

冷酷派的實現從OC的對象創建角度出發,就是把創建對象的各種入口給封死了。alloc,copy等等,無論是采用哪種方式創建,我都保證給出的對象是同一個。
由Objective-C的一些特性可以知道,在對象創建的時候,無論是alloc還是new,都會調用到 allocWithZone方法。在通過拷貝的時候創建對象時,會調用到-(id)copyWithZone:(NSZone *)zone-(id)mutableCopyWithZone:(NSZone *)zone方法。因此,可以重寫這些方法,讓創建的對象唯一。

+(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. 溫柔派的實現

溫柔派就直接告訴外面,alloc,new,copy,mutableCopy方法不可以直接調用。否則編譯不過。

+(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")));

我個人的話比較喜歡采用溫柔派的實現。不需要這么多復雜的實現。也讓使用方有比較明確的概念這個是個單例,不要調皮。對于一般的業務場景是足夠了的。

五、 可不可以再方便點?

可以。
大神們把單例模式的各種套路封裝成了宏。這樣使用的時候,就不需要每個類都手動寫一遍里面的重復代碼了。省去了敲代碼的時間。
以溫柔派的為例,大概是這樣子的。

#define DJ_SINGLETON_DEF(_type_) + (_type_ *)sharedInstance;\
+(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")));\

#define DJ_SINGLETON_IMP(_type_) + (_type_ *)sharedInstance{\
static _type_ *theSharedInstance = nil;\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
theSharedInstance = [[super alloc] init];\
});\
return theSharedInstance;\
}

那么,在定義和實現的時候就很簡單了:

@interface DJSingleton : NSObject
    DJ_SINGLETON_DEF(DJSingleton);
@end

@implementation DJSingleton
    DJ_SINGLETON_IMP(DJSingleton);
@end

六、 單例模式潛在的問題

1. 內存問題

單例模式實際上延長了對象的生命周期。那么就存在內存問題。因為這個對象在程序的整個生命都存在。所以當這個單例比較大的時候,總是hold住那么多內存,就需要考慮這件事了。另外,可能單例本身并不大,但是它如果強引用了另外的比較大的對象,也算是一個問題。別的對象因為單例對象不釋放而不釋放。
當然這個問題也有一定的辦法。比如對于一些可以重新加載的對象,在需要的時候加載,用完之后,單例對象就不再強引用,從而把原先hold住的對象釋放掉。下次需要再加載回來。

2. 循環依賴問題

在開發過程中,單例對象可能有一些屬性,一般會放在init的時候創建和初始化。這樣,比如如果單例A的m屬性依賴于單例B,單例B的屬性n依賴于單例A,初始化的時候就會出現死循環依賴。死在dispatch_once里。


@interface DJSingletonA : NSObject
    DJ_SINGLETON_DEF(DJSingletonA);
@end

@interface DJSingletonB : NSObject
    DJ_SINGLETON_DEF(DJSingletonB);
@end

@interface DJSingletonA()
@property(nonatomic, strong) id someObj;
@end
@implementation DJSingletonA
DJ_SINGLETON_IMP(DJSingletonA);
-(id)init{
    if (self = [super init]) {
        _someObj = [DJSingletonB sharedInstance];
    }
    return self;
}
@end

@interface DJSingletonB()
@property(nonatomic, strong) id someObj;
@end
@implementation DJSingletonB
DJ_SINGLETON_IMP(DJSingletonB);
-(id)init{
    if (self = [super init]) {
        _someObj = [DJSingletonA sharedInstance];
    }
    return self;
}
@end


//---------------------------------------
DJSingletonA * s1 = [DJSingletonA sharedInstance];
死亡現場

對于這種情況,最好的設計是在單例設計的時候,初始化的內容不要依賴于其他對象。如果實在要依賴,就不要讓它形成環。實在會形成環或者無法控制,就采用異步初始化的方式。先過去,內容以后再填。內部需要做個標識,標識這個單例在造出來之后,不能立刻使用或者完整使用。

七、參考資料:

1. Design Patterns -- GOF
2. Pro Objective-C Design Patterns for iOS -- Carlo Chung
3. GCD 中 dispatch_once 的性能與實現

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容

  • @WilliamAlex大叔 前言 目前流行的社交APP中都離不開單例的使用,我們來舉個例子哈,比如現在流行的"糗...
    Alexander閱讀 1,928評論 6 28
  • 單例模式是日常開發工作中經常會用到的一種設計模式。通過單例模式,可以保證程序中的一個類只有一個實例,從而方便對實例...
    狼鳳皇閱讀 191評論 0 0
  • 單例模式的作用:保證在程序運行過程中,一個類只有一個實例對象,節約系統資源。 單例模式使用場合:在整個應用程序中,...
    Xcode10閱讀 366評論 0 0
  • 今天文章的主題是:坐在辦公室里面工作8小時,你知足嗎? “朝八晚六”放下休息時間不算,大概每天工作八小時,實則對于...
    30065閱讀 791評論 0 0
  • 彭小六私密群日更計劃·關于寫作 (1) 作者:陳小星星 、開始寫作。 當你決定要寫作的時候,就把你想到的所有內容都...
    BigQ個人成長閱讀 552評論 0 50