單例模式,由于其簡(jiǎn)單好用容易理解戳寸、同時(shí)在出問(wèn)題時(shí)也容易定位的特點(diǎn),在開(kāi)發(fā)中經(jīng)常用到的一個(gè)設(shè)計(jì)模式努释,本文主要分享我在自己的代碼中是如何使用單例模式的碘梢。
1、什么是單例模式
單例模式的定義
簡(jiǎn)單的來(lái)說(shuō)伐蒂,一個(gè)單例類(lèi)煞躬,在整個(gè)程序中只有一個(gè)實(shí)例,并且提供一個(gè)類(lèi)方法供全局調(diào)用逸邦,在編譯時(shí)初始化這個(gè)類(lèi)恩沛,然后一直保存在內(nèi)存中,到程序(APP)退出時(shí)由系統(tǒng)自動(dòng)釋放這部分內(nèi)存缕减。
系統(tǒng)為我們提供的單例類(lèi)有哪些雷客?
UIApplication(應(yīng)用程序?qū)嵗?lèi))
NSNotificationCenter(消息中心類(lèi))
NSFileManager(文件管理類(lèi))
NSUserDefaults(應(yīng)用程序設(shè)置)
NSURLCache(請(qǐng)求緩存類(lèi))
NSHTTPCookieStorage(應(yīng)用程序cookies池)
在哪些地方會(huì)用到單例模式
一般在我的程序中,經(jīng)常調(diào)用的類(lèi)桥狡,如工具類(lèi)搅裙、公共跳轉(zhuǎn)類(lèi)等妓局,我都會(huì)采用單例模式;
重復(fù)初始化單例類(lèi)會(huì)怎樣呈宇?
請(qǐng)看下面的例子好爬,我在我的工程中,初始化一次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è)單例類(lèi)只能初始化一次。
2融虽、單例類(lèi)的生命周期
單例實(shí)例在存儲(chǔ)器的中位置
請(qǐng)看下面的表格展示了程序中中不同的變量在手機(jī)存儲(chǔ)器中的存儲(chǔ)位置享完;
位置 | 存放的變量 |
---|---|
棧 | 臨時(shí)變量(由編譯器管理自動(dòng)創(chuàng)建/分配/釋放的,棧中的內(nèi)存被調(diào)用時(shí)處于存儲(chǔ)空間中有额,調(diào)用完畢后由系統(tǒng)系統(tǒng)自動(dòng)釋放內(nèi)存) |
堆 | 通過(guò)alloc般又、calloc、malloc或new申請(qǐng)內(nèi)存巍佑,由開(kāi)發(fā)者手動(dòng)在調(diào)用之后通過(guò)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è)單例類(lèi)在程序中只能初始化一次怕膛,為了保證在使用中始終都是存在的,所以單例是在存儲(chǔ)器的全局區(qū)域
丘薛,在編譯時(shí)分配內(nèi)存嘉竟,只要程序還在運(yùn)行就會(huì)一直占用內(nèi)存,在APP結(jié)束后由系統(tǒng)釋放這部分內(nèi)存內(nèi)存洋侨。
3舍扰、新建一個(gè)單例類(lèi)
(1)、單例模式的創(chuàng)建方式希坚;
同步鎖 :NSLock
@synchronized(self) {}
信號(hào)量控制并發(fā):dispatch_semaphore_t
條件鎖:NSConditionLock
dispatch_once_t
考慮數(shù)據(jù)和線程問(wèn)題边苹,蘋(píng)果官方推薦開(kāi)發(fā)者使用dispatch_once_t
來(lái)創(chuàng)建單例,那么我就采用dispatch_once_t
方法來(lái)創(chuàng)建一個(gè)單例裁僧,類(lèi)名為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)
先說(shuō)優(yōu)點(diǎn):
(1)、在整個(gè)程序中只會(huì)實(shí)例化一次茬底,所以在程序如果出了問(wèn)題沪悲,可以快速的定位問(wèn)題所在;
(2)阱表、由于在整個(gè)程序中只存在一個(gè)對(duì)象殿如,節(jié)省了系統(tǒng)內(nèi)存資源,提高了程序的運(yùn)行效率最爬;
再說(shuō)缺點(diǎn)
(1)涉馁、不能被繼承,不能有子類(lèi)爱致;
(2)烤送、不易被重寫(xiě)或擴(kuò)展(可以使用分類(lèi));
(3)糠悯、同時(shí)帮坚,由于單例對(duì)象只要程序在運(yùn)行中就會(huì)一直占用系統(tǒng)內(nèi)存,該對(duì)象在閑置時(shí)并不能銷(xiāo)毀逢防,在閑置時(shí)也消耗了系統(tǒng)內(nèi)存資源叶沛;
5蒲讯、單例模式詳解
(1)忘朝、重寫(xiě)單例類(lèi)的alloc
方法保證這個(gè)類(lèi)只會(huì)被初始化一次
我在viewDidLoad
方法中調(diào)用單例類(lèi)的alloc
和init
方法:
[[OneTimeClass alloc]init];
此時(shí)只是報(bào)黃點(diǎn),但是并沒(méi)有報(bào)錯(cuò)判帮,Run
程序也可以成功局嘁,這樣的話,就不符合我們最開(kāi)始使用單例模式的初衷來(lái)晦墙,這個(gè)類(lèi)也可以隨便初始化類(lèi)悦昵,為什么呢?因?yàn)槲覀儾](méi)有獲取OneTimeClass
類(lèi)的使用實(shí)例晌畅,改進(jìn)代碼:
[OneTimeClass sharedOneTimeClass];
[[OneTimeClass alloc]init];
這是改進(jìn)后的但指,但是在多人開(kāi)發(fā)時(shí),還是沒(méi)辦法保證抗楔,我們會(huì)先調(diào)用alloc
方法棋凳,這樣我們就沒(méi)辦法控制了,但是我們控制OneTimeClass
類(lèi)连躏,此時(shí)我們可以重寫(xiě)OneTimeClass
類(lèi)的alloc
方法,此處在重寫(xiě)alloc
方法的處理可以采用斷言或者系統(tǒng)為開(kāi)發(fā)者提供的NSException類(lèi)來(lái)告訴其他的同事這個(gè)類(lèi)是單例類(lèi)剩岳,不能多次初始化。
//斷言
+ (instancetype)alloc {
NSCAssert(!__onetimeClass, @"OneTimeClass類(lèi)只能初始化一次");
return [super alloc];
}
//NSException
+ (instancetype)alloc {
//如果已經(jīng)初始化了
if (__onetimeClass) {
NSException *exception = [NSException exceptionWithName:@"提示" reason:@"OneTimeClass類(lèi)只能初始化一次" userInfo:nil];
[exception raise];
}
return [super alloc];
}
此時(shí)在run一次入热,可以看到程序直接崩到main函數(shù)上了拍棕,并按照我之前給的提示報(bào)錯(cuò)晓铆。
但是,如果我們的程序直接就崩潰了绰播,這樣的做法與開(kāi)發(fā)者開(kāi)發(fā)APP的初衷是不是又相悖了骄噪,作為一個(gè)程序員的目的要給用戶一個(gè)交互友好的APP,而不是一點(diǎn)小問(wèn)題就崩潰蠢箩,當(dāng)然咯腰池,如果想和測(cè)試的妹紙多交流交流,那就忙芒。示弓。。呵萨。奏属。
對(duì)于這種情況,可以用到NSObect
類(lèi)提供的load
方法和initialize
方法來(lái)控制潮峦,
這兩個(gè)方法的調(diào)用時(shí)機(jī):
load
方法是在整個(gè)文件被加載到運(yùn)行時(shí)囱皿,在main函數(shù)調(diào)用之前調(diào)用;
initialize
方法是在該類(lèi)第一次調(diào)用該類(lèi)時(shí)調(diào)用忱嘹;
為了驗(yàn)證load
方法和initialize
方法的調(diào)用時(shí)機(jī)嘱腥,我在 Main
函數(shù)中打印:
printf("\n\n\n\nmain()");
在OneTimeClass
類(lèi)的load
方法中打泳性谩:
+ (void)load {
printf("\n\nOneTimeClass load()");
}
在OneTimeClass
類(lèi)的initialize
方法中打映萃谩:
+ (void)initialize {
printf("\n\nOneTimeClass initialize()");
}
運(yùn)行程序,最后的結(jié)果是础米,load
方法先打印出來(lái)分苇,所以可以確定的是load
的確是在在main函數(shù)調(diào)用之前調(diào)用的。
這樣的話屁桑,如果我在單例類(lèi)的load
方法或者initialize
方法中初始化這個(gè)類(lèi)医寿,是不是就保證了這個(gè)類(lèi)在整個(gè)程序中調(diào)用一次呢?
+ (void)load {
printf("\n\nOneTimeClass load()");
}
+ (void)initialize {
printf("\nOneTimeClass initialize()\n\n\n");
[OneTimeClass sharedOneTimeClass];
}
這樣就可以保證sharedOneTimeClass
方法是最早調(diào)用的蘑斧。同時(shí)靖秩,再次對(duì)alloc
方法修改,無(wú)論在何時(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出來(lái)的內(nèi)存地址是相同的准浴,這就說(shuō)明此時(shí)我的OneTimeClass
類(lèi)就只初始化了一次事扭。
OneTimeClass *onetime1 = [OneTimeClass sharedOneTimeClass];
NSLog(@"shared:============%@",onetime1);
OneTimeClass *onetime2 = [[OneTimeClass alloc] init];
NSLog(@"new:============%@",onetime2);
(2)、對(duì)new
乐横、copy
求橄、mutableCopy
的處理
方案一:重寫(xiě)這幾個(gè)方法今野,當(dāng)調(diào)用時(shí)提示或者返回
OneTimeClass
類(lèi)實(shí)例,請(qǐng)參考alloc
方法的處理罐农;
方案二:直接禁用這個(gè)方法条霜,禁止調(diào)用這幾個(gè)方法,否則就報(bào)錯(cuò)涵亏,編譯不過(guò)宰睡;
+(instancetype) new __attribute__((unavailable("OneTimeClass類(lèi)只能初始化一次")));
-(instancetype) copy __attribute__((unavailable("OneTimeClass類(lèi)只能初始化一次")));
-(instancetype) mutableCopy __attribute__((unavailable("OneTimeClass類(lèi)只能初始化一次")));
此時(shí)我在viewDidLoad
中調(diào)用new
,然后Build
,編譯器會(huì)直接給出錯(cuò)誤警告气筋,如下圖:
這樣就解決了單例類(lèi)被多次初始化的問(wèn)題拆内;
(3)、分類(lèi)Category
的使用
如果在程序中某個(gè)模塊的業(yè)務(wù)邏輯比較多宠默,此時(shí)可以選擇分類(lèi)Category
的方式麸恍,這樣做的好處是:
(1)、減少Controller
代碼行數(shù)搀矫,使代碼邏輯更清晰抹沪;
(2)、把同一個(gè)功能業(yè)務(wù)區(qū)分開(kāi)瓤球,利于后期的維護(hù)融欧;
(3)、遇到BUG
能快速定位到相關(guān)代碼卦羡;
原則上分類(lèi)Category
只能增加和實(shí)現(xiàn)方法噪馏,而不能增加屬性,此處請(qǐng)參考美團(tuán)技術(shù)團(tuán)隊(duì)的博客:深入理解Objective-C:Category
例如虹茶,在我們的APP中逝薪,用到了Socket
技術(shù),我在客戶端Socket
部分的代碼使用了單例模式蝴罪。由于和服務(wù)器的交互比較多,此時(shí)采用分類(lèi)Category
的方式步清,把Socket
異常處理要门,給服務(wù)器發(fā)送的協(xié)議,和接受到服務(wù)器的協(xié)議 用三個(gè)分類(lèi)Category
來(lái)實(shí)現(xiàn)廓啊。在以后的維護(hù)中如果業(yè)務(wù)復(fù)雜度增加欢搜,或者加了新的業(yè)務(wù)或功能,可繼續(xù)新建一個(gè)分類(lèi)谴轮。這樣既不影響之前的代碼炒瘟,同時(shí)又可以保證新的代碼邏輯清晰。
以上是我在單例模式使用上的一些總結(jié)第步,如果有錯(cuò)誤的地方疮装,請(qǐng)指出缘琅。
本文demo:戳這里
本文參考:細(xì)說(shuō)@synchronized和dispatch_once