單例模式,由于其簡單好用容易理解、同時在出問題時也容易定位的特點,在開發(fā)中經(jīng)常用到的一個設(shè)計模式,本文主要分享我在自己的代碼中是如何使用單例模式的。
1、什么是單例模式
單例模式的定義
簡單的來說,一個單例類,在整個程序中只有一個實例,并且提供一個類方法供全局調(diào)用,在編譯時初始化這個類,然后一直保存在內(nèi)存中,到程序(APP)退出時由系統(tǒng)自動釋放這部分內(nèi)存。
系統(tǒng)為我們提供的單例類有哪些?
UIApplication(應(yīng)用程序?qū)嵗?
NSNotificationCenter(消息中心類)
NSFileManager(文件管理類)
NSUserDefaults(應(yīng)用程序設(shè)置)
NSURLCache(請求緩存類)
NSHTTPCookieStorage(應(yīng)用程序cookies池)
在哪些地方會用到單例模式
一般在我的程序中,經(jīng)常調(diào)用的類,如工具類、公共跳轉(zhuǎn)類等,我都會采用單例模式;
重復(fù)初始化單例類會怎樣?
請看下面的例子,我在我的工程中,初始化一次UIApplication,
[[UIApplication alloc]init];
最后運行的結(jié)果是,程序直接崩潰,并報了下面的錯,
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'There can only be one UIApplication instance.'

所以,由此可以確定,一個單例類只能初始化一次。
2、單例類的生命周期
單例實例在存儲器的中位置
請看下面的表格展示了程序中中不同的變量在手機(jī)存儲器中的存儲位置;
| 位置 | 存放的變量 |
|---|---|
| 棧 | 臨時變量(由編譯器管理自動創(chuàng)建/分配/釋放的,棧中的內(nèi)存被調(diào)用時處于存儲空間中,調(diào)用完畢后由系統(tǒng)系統(tǒng)自動釋放內(nèi)存) |
| 堆 | 通過alloc、calloc、malloc或new申請內(nèi)存,由開發(fā)者手動在調(diào)用之后通過free或delete釋放內(nèi)存。動態(tài)內(nèi)存的生存期可以由我們決定,如果我們不釋放內(nèi)存,程序?qū)⒃谧詈蟛裴尫诺魟討B(tài)內(nèi)存,在ARC模式下,由系統(tǒng)自動管理。 |
| 全局區(qū)域 | 靜態(tài)變量(編譯時分配,APP結(jié)束時由系統(tǒng)釋放) |
| 常量 | 常量(編譯時分配,APP結(jié)束時由系統(tǒng)釋放) |
| 代碼區(qū) | 存放代碼 |
在程序中,一個單例類在程序中只能初始化一次,為了保證在使用中始終都是存在的,所以單例是在存儲器的全局區(qū)域,在編譯時分配內(nèi)存,只要程序還在運行就會一直占用內(nèi)存,在APP結(jié)束后由系統(tǒng)釋放這部分內(nèi)存內(nèi)存。
3、新建一個單例類
(1)、單例模式的創(chuàng)建方式;
同步鎖 :NSLock
@synchronized(self) {}
信號量控制并發(fā):dispatch_semaphore_t
條件鎖:NSConditionLock
dispatch_once_t
考慮數(shù)據(jù)和線程問題,蘋果官方推薦開發(fā)者使用dispatch_once_t來創(chuàng)建單例,那么我就采用dispatch_once_t方法來創(chuàng)建一個單例,類名為OneTimeClass。
static OneTimeClass *__onetimeClass;
+ (OneTimeClass *)sharedOneTimeClass {
static dispatch_once_t oneToken;
dispatch_once(&oneToken, ^{
__onetimeClass = [[OneTimeClass alloc]init];
});
return __onetimeClass;
}
4、單例模式的優(yōu)缺點
先說優(yōu)點:
(1)、在整個程序中只會實例化一次,所以在程序如果出了問題,可以快速的定位問題所在;
(2)、由于在整個程序中只存在一個對象,節(jié)省了系統(tǒng)內(nèi)存資源,提高了程序的運行效率;
再說缺點
(1)、不能被繼承,不能有子類;
(2)、不易被重寫或擴(kuò)展(可以使用分類);
(3)、同時,由于單例對象只要程序在運行中就會一直占用系統(tǒng)內(nèi)存,該對象在閑置時并不能銷毀,在閑置時也消耗了系統(tǒng)內(nèi)存資源;
5、單例模式詳解
(1)、重寫單例類的alloc方法保證這個類只會被初始化一次
我在viewDidLoad方法中調(diào)用單例類的alloc和init方法:
[[OneTimeClass alloc]init];
此時只是報黃點,但是并沒有報錯,Run程序也可以成功,這樣的話,就不符合我們最開始使用單例模式的初衷來,這個類也可以隨便初始化類,為什么呢?因為我們并沒有獲取OneTimeClass類的使用實例,改進(jìn)代碼:
[OneTimeClass sharedOneTimeClass];
[[OneTimeClass alloc]init];
這是改進(jìn)后的,但是在多人開發(fā)時,還是沒辦法保證,我們會先調(diào)用alloc方法,這樣我們就沒辦法控制了,但是我們控制OneTimeClass類,此時我們可以重寫OneTimeClass類的alloc方法,此處在重寫alloc方法的處理可以采用斷言或者系統(tǒng)為開發(fā)者提供的NSException類來告訴其他的同事這個類是單例類,不能多次初始化。
//斷言
+ (instancetype)alloc {
NSCAssert(!__onetimeClass, @"OneTimeClass類只能初始化一次");
return [super alloc];
}
//NSException
+ (instancetype)alloc {
//如果已經(jīng)初始化了
if (__onetimeClass) {
NSException *exception = [NSException exceptionWithName:@"提示" reason:@"OneTimeClass類只能初始化一次" userInfo:nil];
[exception raise];
}
return [super alloc];
}
此時在run一次,可以看到程序直接崩到main函數(shù)上了,并按照我之前給的提示報錯。

但是,如果我們的程序直接就崩潰了,這樣的做法與開發(fā)者開發(fā)APP的初衷是不是又相悖了,作為一個程序員的目的要給用戶一個交互友好的APP,而不是一點小問題就崩潰,當(dāng)然咯,如果想和測試的妹紙多交流交流,那就。。。。。
對于這種情況,可以用到NSObect類提供的load方法和initialize方法來控制,
這兩個方法的調(diào)用時機(jī):
load方法是在整個文件被加載到運行時,在main函數(shù)調(diào)用之前調(diào)用;
initialize方法是在該類第一次調(diào)用該類時調(diào)用;
為了驗證load方法和initialize方法的調(diào)用時機(jī),我在 Main函數(shù)中打?。?/p>
printf("\n\n\n\nmain()");
在OneTimeClass類的load方法中打印:
+ (void)load {
printf("\n\nOneTimeClass load()");
}
在OneTimeClass類的initialize方法中打?。?/p>
+ (void)initialize {
printf("\n\nOneTimeClass initialize()");
}
運行程序,最后的結(jié)果是,load方法先打印出來,所以可以確定的是load的確是在在main函數(shù)調(diào)用之前調(diào)用的。

這樣的話,如果我在單例類的load方法或者initialize方法中初始化這個類,是不是就保證了這個類在整個程序中調(diào)用一次呢?
+ (void)load {
printf("\n\nOneTimeClass load()");
}
+ (void)initialize {
printf("\nOneTimeClass initialize()\n\n\n");
[OneTimeClass sharedOneTimeClass];
}
這樣就可以保證sharedOneTimeClass方法是最早調(diào)用的。同時,再次對alloc方法修改,無論在何時調(diào)用OneTimeClass已經(jīng)初始化了,如果再次調(diào)用alloc可直接返回__onetimeClass實例。
+ (instancetype)alloc {
if (__onetimeClass) {
return __onetimeClass;
}
return [super alloc];
}
最后在ViewController中打印調(diào)用OneTimeClass的sharedOneTimeClass和alloc方法,可以看到Log出來的內(nèi)存地址是相同的,這就說明此時我的OneTimeClass類就只初始化了一次。
OneTimeClass *onetime1 = [OneTimeClass sharedOneTimeClass];
NSLog(@"shared:============%@",onetime1);
OneTimeClass *onetime2 = [[OneTimeClass alloc] init];
NSLog(@"new:============%@",onetime2);

(2)、對new、copy、mutableCopy的處理
方案一:重寫這幾個方法,當(dāng)調(diào)用時提示或者返回
OneTimeClass類實例,請參考alloc方法的處理;
方案二:直接禁用這個方法,禁止調(diào)用這幾個方法,否則就報錯,編譯不過;
+(instancetype) new __attribute__((unavailable("OneTimeClass類只能初始化一次")));
-(instancetype) copy __attribute__((unavailable("OneTimeClass類只能初始化一次")));
-(instancetype) mutableCopy __attribute__((unavailable("OneTimeClass類只能初始化一次")));
此時我在viewDidLoad中調(diào)用new,然后Build,編譯器會直接給出錯誤警告,如下圖:

這樣就解決了單例類被多次初始化的問題;
(3)、分類Category的使用
如果在程序中某個模塊的業(yè)務(wù)邏輯比較多,此時可以選擇分類Category的方式,這樣做的好處是:
(1)、減少Controller代碼行數(shù),使代碼邏輯更清晰;
(2)、把同一個功能業(yè)務(wù)區(qū)分開,利于后期的維護(hù);
(3)、遇到BUG能快速定位到相關(guān)代碼;
原則上分類Category只能增加和實現(xiàn)方法,而不能增加屬性,此處請參考美團(tuán)技術(shù)團(tuán)隊的博客:深入理解Objective-C:Category
例如,在我們的APP中,用到了Socket技術(shù),我在客戶端Socket部分的代碼使用了單例模式。由于和服務(wù)器的交互比較多,此時采用分類Category的方式,把Socket異常處理,給服務(wù)器發(fā)送的協(xié)議,和接受到服務(wù)器的協(xié)議 用三個分類Category來實現(xiàn)。在以后的維護(hù)中如果業(yè)務(wù)復(fù)雜度增加,或者加了新的業(yè)務(wù)或功能,可繼續(xù)新建一個分類。這樣既不影響之前的代碼,同時又可以保證新的代碼邏輯清晰。
以上是我在單例模式使用上的一些總結(jié),如果有錯誤的地方,請指出。
本文demo:戳這里
本文參考:細(xì)說@synchronized和dispatch_once