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

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

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

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

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

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