在iOS開發(fā)中呐萨,經(jīng)常使用到單例。單例是Cocoa中被廣泛使用的設(shè)計模式之一莽囤。單例使得某個類在整個application的生命周期中只有一個實(shí)例谬擦,減少內(nèi)存開銷,統(tǒng)一了某些具體操作的邏輯烁登,方便管理怯屉。開發(fā)中常用構(gòu)造單例的方法有兩種@sychronized
和dispatch_once
。
@sychronized
使用@sychronized
構(gòu)造單例通常如下:
static SomeClass * instance = nil;
+ (instancetype)shareInstance {
@synchronized(self) {
if (instance == nil) {
instance = [[SomeClass alloc] init];
}
}
return instance;
}
@sychronized
是一個編譯器指令饵沧,方便我們對臨界區(qū)提供互斥鎖(mutex locks )锨络。也就是說在多線程并發(fā)訪問臨界區(qū)(@synchronized(self) {//臨界區(qū)}
)時,它保證同一時刻只有一個線程處于臨界區(qū)中狼牺,其他線程阻塞等待羡儿。那么@sychronized
如何標(biāo)識一個互斥鎖?蘋果文檔中說了是钥,@sychronized
可以使用任意的Objective-C對象作為互斥鎖的標(biāo)識符(lock token)掠归。那么問題來了,如果互斥鎖的標(biāo)識符不一樣呢(動態(tài)的)悄泥?比如下面這樣:
- (void)instantMethod:(id)lockTocken
{
@synchronized(lockTocken)
{
//臨界區(qū)
}
}
假設(shè)現(xiàn)在有多個線程并發(fā)調(diào)用上面的實(shí)例方法- (void)instantMethod:(id)lockTocken;
虏冻,它們分別為A、B弹囚、C和D線程厨相,其中A、B調(diào)用該方法時候傳入的lockTocken
一樣,C蛮穿、D傳入的lockTocken
一樣庶骄。那么答案是只有標(biāo)識變量(lockTocken
)一樣的線程才會互斥,標(biāo)識變量(lockTocken
)不一樣的線程相互之間沒有影響践磅〉サ螅回到最早的例子,其中使用了self
(類對象)作為互斥鎖的標(biāo)識符府适,由此可見羔飞,多進(jìn)程并發(fā)訪問,使用的互斥鎖是一樣的细溅,并且在第一個進(jìn)入臨界區(qū)的線程初始化instance
后褥傍,其后進(jìn)入的線程就不會再次初始化(instance
不再是nil
)儡嘶,保證了SomeClass
類只有一個實(shí)例喇聊。
好奇的你一定會想,既然
@synchronized
是編譯器指令蹦狂,那么編譯器對這段代碼做了什么誓篱?
有如下代碼:
id obj = ...
@synchronized(obj) {
//臨界區(qū)
}
clang -rewrite-objc之后:
id obj = ...
{ id _rethrow = 0; id _sync_obj = (id)obj; objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);
} catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}
即編譯器把@synchronized
相關(guān)代碼轉(zhuǎn)成下面這一大坨東西。代碼的關(guān)鍵在兩個函數(shù)的調(diào)用:objc_sync_exit
和objc_sync_enter
凯楔。代碼的邏輯還是比較好理解窜骄,想深入了解的可以看下這篇博文。
dispatch_once
使用dispatch_once
構(gòu)造單例通常如下:
static SomeClass * instance = nil;
+ (instancetype)shareInstance {
static dispatch_once_t onceTocken;
dispatch_once(&onceTocken, ^{
instance = [[SomeClass alloc] init];
});
return instance;
}
dispatch_once
是GCD中提供的函數(shù)摆屯,通常使用它來初始化全局?jǐn)?shù)據(jù)(單例)邻遏,它接受兩個參數(shù),dispatch_once_t *
類型的謂語和dispatch_block_t
類型的block(block中的代碼就是臨界區(qū))虐骑。文檔中說到准验,dispatch_once_t *
類型的謂語用于測試block是否已經(jīng)執(zhí)行結(jié)束或者還沒與執(zhí)行,它配合上dispatch_once
函數(shù)保證了在applecation的生命周期中block只會運(yùn)行一次廷没,并且是線程安全的糊饱。可以看到dispatch_once
和@synchronized
一樣颠黎,是線程安全另锋,不同指出在于@synchronized
的臨界區(qū)代碼可能在application生命周期中多次調(diào)用,而dispatch_once
只會調(diào)用一次(使用dispatch_once_t *
類型的謂語做判斷)狭归。因此@synchronized
的臨界區(qū)代碼要判斷instance
是否是nil
夭坪,來判斷是實(shí)例是否已經(jīng)構(gòu)造了。
注意:
dispatch_once_t *
類型的謂語必須是全局變量或者靜態(tài)變量过椎,如果使用自動或者動態(tài)變量(包括Objective-C實(shí)例變量)室梅,dispatch_once
的結(jié)果是無法預(yù)知的。
單例思考
單例用著用著就被濫用了。最近正在思考如何對公司APP的分享模塊重構(gòu)竞惋。其中有個類叫SSShareManager
柜去,作為的單例,它管理著所有模塊的分享邏輯拆宛。濫用點(diǎn)就在既然是單例嗓奢,卻使用它來保存來自不同模塊的數(shù)據(jù)以及狀態(tài)(比如:分享到微信還是QQ、友盟統(tǒng)計的數(shù)據(jù)等)浑厚,無形中增加了不同模塊之間的耦合股耽。因?yàn)樵谀硞€模塊調(diào)用SSShareManager
單例分享之前,還要記得清理其他模塊可能留下的數(shù)據(jù)=钳幅,=物蝙。這篇博文給我很大啟發(fā),其中印象深刻的一段話:
The lesson here is that singletons should be preserved only for state that is global, and not tied to any scope. If state is scoped to any session shorter than “a complete lifecycle of my app,” that state should not be managed by a singleton.