iOS 單例
一、單例介紹
單例:該類在程序運(yùn)行期間有且僅有一個(gè)實(shí)例原朝。
1.1 單例模式的要點(diǎn)#
- 該類有且只有一個(gè)實(shí)例;
- 該類必須能夠自行創(chuàng)建這個(gè)實(shí)例治笨;
- 該類必須能夠自行向整個(gè)系統(tǒng)提供這個(gè)實(shí)例易迹。
1.2 單例的主要優(yōu)點(diǎn)#
- 單例可以保證系統(tǒng)中該類有且僅有一個(gè)實(shí)例,確保所有對(duì)象都訪問這個(gè)唯一實(shí)例幅狮;
- 因?yàn)轭惪刂屏藢?shí)例化過程,所以類可以靈活更改實(shí)例化過程株灸;
- 基于第 1 條崇摄,對(duì)于項(xiàng)目中的個(gè)別場景的傳值、存儲(chǔ)狀態(tài)等業(yè)務(wù)更加方便慌烧;
- 可以節(jié)約系統(tǒng)資源逐抑,對(duì)于一些需要頻繁創(chuàng)建和銷毀的對(duì)象單例模式無疑可以提高系統(tǒng)的性能。
1.3 單例的主要缺點(diǎn)#
- 由于單利模式中沒有抽象層杏死,因此單例類的擴(kuò)展有很大的困難泵肄。單例不能被繼承,不能有子類淑翼;
- 不易被重寫或擴(kuò)展(可以使用分類)
- 單例實(shí)例一旦創(chuàng)建腐巢,對(duì)象指針是保存在靜態(tài)區(qū),那么在堆區(qū)分配的空間只有在應(yīng)用程序終止后才會(huì)被釋放玄括;
- 單例類的職責(zé)過重冯丙,在一定程度上違背了“單一職責(zé)原則”。
1.4 單例的生命周期#
下面的表格展示了程序中中不同的變量在手機(jī)存儲(chǔ)器中的存儲(chǔ)位置遭京;
位置 | 存放的變量 |
---|---|
棧 | 臨時(shí)變量(由編譯器管理自動(dòng)創(chuàng)建/分配/釋放的胃惜,棧中的內(nèi)存被調(diào)用時(shí)處于存儲(chǔ)空間中,調(diào)用完畢后由系統(tǒng)系統(tǒng)自動(dòng)釋放內(nèi)存) |
堆 | 通過 alloc哪雕、calloc船殉、malloc 或 new 申請(qǐng)內(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è)單例類在程序中只能初始化一次,為了保證在使用中始終都是存在的固阁,所以單例是在存儲(chǔ)器的全局區(qū)域壤躲,在編譯時(shí)分配內(nèi)存,只要程序還在運(yùn)行就會(huì)一直占用內(nèi)存备燃,在 APP 結(jié)束后由系統(tǒng)釋放這部分內(nèi)存內(nèi)存柒爵。
單例的靜態(tài)變量被置為 nil,是否內(nèi)存會(huì)得到釋放赚爵?
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`static Singletion * singleton;
- (void)dealloc
{
NSLog(@"%s", func);
}
Singleton * s = [Singleton sharedSingleton];
s = nil;
singleton = nil;` </pre>
將單例類實(shí)例對(duì)象賦值 nil 后,會(huì)觸發(fā)單例的 dealloc 方法法瑟。
靜態(tài)變量修飾的指針保存在了全局區(qū)域冀膝,不會(huì)被釋放。但是指針保存的首地址關(guān)聯(lián)的對(duì)象是保存在堆區(qū)的霎挟,是會(huì)被釋放的窝剖。
二、單例的實(shí)現(xiàn)
單例的實(shí)現(xiàn)重點(diǎn)就是防止在外部調(diào)用的時(shí)候出現(xiàn)多個(gè)不同的實(shí)例酥夭,也就是說要從創(chuàng)建的方式入手禁止出現(xiàn)多個(gè)不同的實(shí)例赐纱。
主要是做到以下幾點(diǎn):
防止調(diào)用 [[A alloc] init] 引起的錯(cuò)誤
防止調(diào)用 new 引起的錯(cuò)誤
防止調(diào)用 copy 引起的錯(cuò)誤
防止調(diào)用 mutableCopy 引起的錯(cuò)誤
2.1 實(shí)現(xiàn)方式一#
把所有可能出現(xiàn)的初始化方法做了相應(yīng)的處理來其保證安全性
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`+ (instancetype)sharedSingleton
{
static Singleton *_sharedSingleton = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 不能再使用 alloc 方法
// 因?yàn)橐呀?jīng)重寫了 allocWithZone 方法,所以這里要調(diào)用父類的分配空間的方法
_sharedSingleton = [[super allocWithZone:NULL] init];
});
return _sharedSingleton;
}
// ②熬北、防止 [[A alloc] init] 和 new 引起的錯(cuò)誤疙描。因?yàn)?[[A alloc] init] 和 new 實(shí)際是一樣的工作原理,都是執(zhí)行了下面方法
- (instancetype)allocWithZone:(struct _NSZone *)zone
{
return [Singleton sharedSingleton];
}
// ③讶隐、NSCopying 防止 copy 引起的錯(cuò)誤起胰。當(dāng)你的單例類不遵循 NSCopying 協(xié)議,外部調(diào)用本身就會(huì)出錯(cuò).
- (id)copyWithZone:(nullable NSZone *)zone
{
return [Singleton sharedSingleton];
}
// ④巫延、防止 mutableCopy 引起的錯(cuò)誤效五,當(dāng)你的單例類不遵循 NSMutableCopying 協(xié)議,外部調(diào)用本身就會(huì)出錯(cuò).
- (id)mutableCopyWithZone:(nullable NSZone *)zone
{
return [Singleton sharedSingleton];
}` </pre>
dispatch_once 主要是根據(jù) onceToken
的值來決定怎么去執(zhí)行代碼炉峰。
- 當(dāng) onceToken = 0 時(shí)畏妖,線程執(zhí)行 dispatch_once 的 block 中代碼;
- 當(dāng) onceToken = -1 時(shí)疼阔,線程跳過 dispatch_once 的 block 中代碼不執(zhí)行戒劫;
- 當(dāng) onceToken 為其他值時(shí)半夷,線程被阻塞,等待 onceToken 值改變谱仪。
當(dāng)線程調(diào)用 shareInstance玻熙,此時(shí) onceToken = 0,調(diào)用 block 中的代碼疯攒,此時(shí) onceToken 的值變?yōu)?140734537148864嗦随。當(dāng)其他線程再調(diào)用 shareInstance 方法時(shí),onceToken 的值已經(jīng)是 140734537148864 了敬尺,線程阻塞枚尼。當(dāng) block 線程執(zhí)行完 block 之后,onceToken 變?yōu)?-1砂吞,其他線程不再阻塞署恍,跳過 block。下次再調(diào)用 shareInstance 時(shí)蜻直,block 已經(jīng)為 -1盯质,直接跳過 block。
2.2 實(shí)現(xiàn)方式二#
不做處理的情況下禁止外部調(diào)用
一些成熟的第三方代碼的單例中也有使用該方法的概而。
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`.h 文件
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)new NS_UNAVAILABLE;
- (id)copy NS_UNAVAILABLE;
- (id)mutableCopy NS_UNAVAILABLE;
.m 文件
- (instancetype)sharedSingleton
{
static Singleton *_sharedSingleton = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedSingleton = [[self alloc] init]; // 要使用 self 來調(diào)用
});
return _sharedSingleton;
}` </pre>
當(dāng)運(yùn)行 [[A alloc] init] 或 [A new] 時(shí)呼巷,會(huì)直接報(bào)錯(cuò) 'init' is unavailable 或 'new' is unavailable。
三赎瑰、單例的濫用
3.1 全局狀態(tài)#
大多數(shù)的開發(fā)者都認(rèn)同使用全局可變的狀態(tài)是不好的行為王悍。有狀態(tài)使得程序難以理解和難以調(diào)試庇忌。面向?qū)ο蟮某绦騿T在最小化代碼的有狀態(tài)性方面纲熏,有很多還需要向函數(shù)式編程學(xué)習(xí)的地方。
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`@implementation SPMath
{
NSInteger _a;
NSInteger _b;
}
- (NSInteger)add
{
return _a + _b;
}` </pre>
在上面這個(gè)簡單的數(shù)學(xué)庫的實(shí)現(xiàn)中苍息,程序員需要在調(diào)用 add 前正確的設(shè)置實(shí)例變量 _a 和 _b源譬。這樣有以下問題:
add 沒有顯式的通過使用參數(shù)的形式聲明它依賴于 _a 和 _b 的狀態(tài)集惋。與僅僅通過查看函數(shù)聲明就可以知道這個(gè)函數(shù)的輸出依賴于哪些變量不同的是,另一個(gè)開發(fā)者必須查看這個(gè)函數(shù)的具體實(shí)現(xiàn)才能明白這個(gè)函數(shù)依賴那些變量踩娘。隱藏依賴是不好的芋膘。
當(dāng)修改 _a 和 _b 的數(shù)值為調(diào)用 add 做準(zhǔn)備時(shí),程序員需要保證修改不會(huì)影響任何其他依賴于這兩個(gè)變量的代碼的正確性霸饲。而這在多線程的環(huán)境中是尤其困難的为朋。
把下面的代碼和上面的例子做對(duì)比:
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
+ (NSUInteger)addOf:(NSUInteger)a plus:(NSUInteger)b { return a + b; }
</pre>
這里,對(duì)變量 a 和 b 的依賴被顯式的聲明了厚脉,并且不需要為了調(diào)用這個(gè)方法而去改變實(shí)例變量的狀態(tài)习寸,也不需要擔(dān)心調(diào)用這個(gè)函數(shù)會(huì)留下持久的副作用。甚至可以聲明為類方法傻工,這樣就顯式的告訴了代碼的閱讀者:這個(gè)方法不會(huì)修改任何實(shí)例的狀態(tài)霞溪。
那么孵滞,這個(gè)例子和單例相比又有什么關(guān)系呢?用 Mi?ko Hevery 的話來說鸯匹,“單例就是披著羊皮的全局狀態(tài)” 坊饶。
一個(gè)單例可以在不需要顯式聲明對(duì)其依賴的情況下,被使用在任何地方殴蓬。就像變量 _a 和 _b 在 add 內(nèi)部被使用了匿级,卻沒有被顯式聲明一樣,程序的任意模塊都可以調(diào)用 [A sharedInstance] 并且訪問這個(gè)單例染厅。這意味著任何和這個(gè)單例交互產(chǎn)生的副作用都會(huì)影響程序其他地方的任意代碼痘绎。
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`@interface Singleton : NSObject
- (instancetype)sharedInstance;
- (NSString *)name;
- (void)setName:(NSString *)name;
@end
@implementation A
- (void)a
{
if ([[Singleton sharedInstance] name]) {
// ...
}
}
@end
@implementation B
- (void)b
{
[[Singleton sharedInstance] setName:""];
}
@end` </pre>
在上面的代碼中,A 和 B 是兩個(gè)完全獨(dú)立的模塊肖粮。但是 B 可以通過使用單例提供的共享狀態(tài)來影響 A 的行為孤页。這種情況應(yīng)該只能發(fā)生在 B 顯式引用了 A,顯式建立了它們兩者之間的關(guān)系時(shí)涩馆。由于這里使用了單例行施,單例的全局性和有狀態(tài)性,導(dǎo)致隱式的在兩個(gè)看起來完全不相關(guān)的模塊之間建立了耦合魂那。
來看一個(gè)更具體的例子悲龟,并且暴露一個(gè)使用全局可變狀態(tài)的額外問題。
想要在我們的應(yīng)用中構(gòu)建一個(gè)網(wǎng)頁查看器(web viewer)冰寻。我們構(gòu)建了一個(gè)簡單的 URL cache 來支持這個(gè)網(wǎng)頁查看器:
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`@interface URLCache
- (NSCache *)sharedURLCache;
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
@end` </pre>
這個(gè)開發(fā)者開始寫了一些單元測(cè)試來保證代碼在不同的情況下都能達(dá)到預(yù)期。首先皿渗,他寫了一個(gè)測(cè)試用例來保證網(wǎng)頁查看器在沒有設(shè)備鏈接時(shí)能夠展示出錯(cuò)誤信息斩芭。然后他寫了一個(gè)測(cè)試用例來保證網(wǎng)頁查看器能夠正確的處理服務(wù)器錯(cuò)誤。最后乐疆,他為成功情況時(shí)寫了一個(gè)測(cè)試用例划乖,來保證返回的網(wǎng)絡(luò)內(nèi)容能夠被正確的顯示出來。這個(gè)開發(fā)者運(yùn)行了所有的測(cè)試用例挤土,并且它們都如預(yù)期一樣正確琴庵。
幾個(gè)月以后,這些測(cè)試用例開始出現(xiàn)失敗仰美,盡管網(wǎng)頁查看器的代碼從它寫完后就從來沒有再改動(dòng)過迷殿!到底發(fā)生了什么?
原來咖杂,有人改變了測(cè)試的順序庆寺。處理成功的那個(gè)測(cè)試用例首先被運(yùn)行,然后再運(yùn)行其他兩個(gè)诉字。處理錯(cuò)誤的那兩個(gè)測(cè)試用例現(xiàn)在竟然成功了懦尝,和預(yù)期不一樣知纷,因?yàn)?URL cache 這個(gè)單例把不同測(cè)試用例之間的 response 緩存起來了。
持久化狀態(tài)是單元測(cè)試的敵人陵霉,因?yàn)閱卧獪y(cè)試在各個(gè)測(cè)試用例相互獨(dú)立的情況下才有效琅轧。如果狀態(tài)從一個(gè)測(cè)試用例傳遞到了另外一個(gè),這樣就和測(cè)試用例的執(zhí)行順序就有關(guān)系了踊挠。有 bug 的測(cè)試用例是非常糟糕的事情乍桂,特別是那些有時(shí)候能通過測(cè)試,有時(shí)候又不能通過測(cè)試的止毕。
3.2 對(duì)象的生命周期#
另外一個(gè)關(guān)鍵問題就是單例的生命周期模蜡。當(dāng)你在程序中添加一個(gè)單例時(shí),很容易會(huì)認(rèn)為 “它們永遠(yuǎn)只能有一個(gè)實(shí)例”扁凛。但是在很多我看到過的 iOS 代碼中忍疾,這種假定都可能被打破。
假設(shè)我們正在構(gòu)建一個(gè)應(yīng)用谨朝,在這個(gè)應(yīng)用里用戶可以看到他們的好友列表卤妒。他們的每個(gè)朋友都有一張個(gè)人信息的圖片,并且我們想使我們的應(yīng)用能夠下載并且在設(shè)備上緩存這些圖片字币。 使用 dispatch_once 代碼片段则披,寫一個(gè) ThumbnailCache 單例:
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`@interface ThumbnailCache : NSObject
- (instancetype)sharedThumbnailCache;
- (void)cacheProfileImage:(NSData *)imageData forUserId:(NSString *)userId;
- (NSData *)cachedProfileImageForUserId:(NSString *)userId;
@end` </pre>
繼續(xù)構(gòu)建我們的應(yīng)用,一切看起來都很正常洗出,直到有一天士复,決定實(shí)現(xiàn)“注銷”功能時(shí),這樣用戶可以在應(yīng)用中進(jìn)行賬號(hào)切換翩活。突然發(fā)現(xiàn)我們將要面臨一個(gè)討厭的問題:用戶相關(guān)的狀態(tài)存儲(chǔ)在全局單例中阱洪。
當(dāng)用戶注銷后,我們希望能夠清理掉所有的硬盤上的持久化狀態(tài)菠镇。否則冗荸,我們將會(huì)把這些被遺棄的數(shù)據(jù)殘留在用戶的設(shè)備上,浪費(fèi)寶貴的硬盤空間利耍。對(duì)于用戶登出又登錄了一個(gè)新的賬號(hào)這種情況蚌本,我們也想能夠?qū)@個(gè)新用戶使用一個(gè)全新的 ThumbnailCache 實(shí)例。
問題在于按照定義單例被認(rèn)為是“創(chuàng)建一次隘梨,永久有效”的實(shí)例程癌。你可以想到一些對(duì)于上述問題的解決方案≈崃裕或許我們可以在用戶登出時(shí)移除這個(gè)單例:
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`static ThumbnailCache * sharedThumbnailCache;
(instancetype)sharedThumbnailCache
{
if (!sharedThumbnailCache) {
sharedThumbnailCache = [[self alloc] init];
}
return sharedThumbnailCache;
}(void)cleanUp
{
// The SPThumbnailCache will clean up persistent states when deallocated
sharedThumbnailCache = nil;
}` </pre>
這是一個(gè)明顯的對(duì)單例模式的濫用席楚,但是它可以工作,對(duì)吧税稼。
當(dāng)然可以使用這種方式去解決烦秩,但代價(jià)實(shí)在是太大了垮斯。我們不能使用簡單的、能夠保證線程安全和所有的調(diào)用 [ThumbnailCache sharedThumbnailCache] 的地方都會(huì)訪問同一個(gè)實(shí)例的 dispatch_once 解決方案了≈混簦現(xiàn)在我們需要對(duì)使用 thumbnail cache 時(shí)的代碼的執(zhí)行順序非常小心兜蠕。假設(shè)當(dāng)用戶正在執(zhí)行登出操作時(shí),有一些后臺(tái)任務(wù)正在執(zhí)行把圖片保存到緩存中的操作:
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[ThumbnailCache sharedThumbnailCache] cacheProfileImage:newImage forUserId:userId]; });
</pre>
需要保證在所有的后臺(tái)任務(wù)完成前抛寝, cleanUp 一定不能被執(zhí)行熊杨。這保證了 newImage 可以被正確的清理掉〉两ⅲ或者晶府,我們需要保證在 thumbnail cache 被移除時(shí),后臺(tái)緩存任務(wù)一定要被取消掉钻趋。否則川陆,一個(gè)新的 thumbnail cache 的實(shí)例將會(huì)被延遲創(chuàng)建,并且之前用戶的數(shù)據(jù)(newImage 對(duì)象)會(huì)被存儲(chǔ)在它里面蛮位。
由于對(duì)于單例實(shí)例來說它沒有明確的所有者较沪,(比如,單例自己管理自己的生命周期)失仁,永遠(yuǎn)“關(guān)閉”一個(gè)單例變得非常的困難尸曼。
分析到這里,希望能夠意識(shí)到萄焦,這個(gè) thumbnail cache 從來就不應(yīng)該作為一個(gè)單例控轿。問題在于一個(gè)對(duì)象的生命周期可能在項(xiàng)目的最初階段沒有被很好得考慮清楚。
舉一個(gè)具體的例子拂封,Dropbox 的 iOS 客戶端曾經(jīng)只支持一個(gè)賬號(hào)登錄茬射。它以這樣的狀態(tài)存在了數(shù)年,直到有一天我們希望能夠同時(shí)支持多個(gè)用戶賬號(hào)登錄(既包括個(gè)人賬號(hào)也包括企業(yè)賬號(hào))烘苹。突然之間,我們以前的的假設(shè)“只能夠同時(shí)有一個(gè)用戶處于登錄狀態(tài)”就不成立了片部。 假定一個(gè)對(duì)象的生命周期和應(yīng)用的生命周期一致镣衡,會(huì)限制你的代碼的靈活擴(kuò)展,早晚有一天當(dāng)產(chǎn)品的需求產(chǎn)生變化時(shí)档悠,你會(huì)為當(dāng)初的這個(gè)假定付出代價(jià)的廊鸥。
這里我們得到的教訓(xùn)是:單例應(yīng)該只用來保存全局的狀態(tài),并且不能和任何作用域綁定辖所。如果這些狀態(tài)的作用域比一個(gè)完整的應(yīng)用程序的生命周期要短惰说,那么這個(gè)狀態(tài)就不應(yīng)該使用單例來管理。用一個(gè)單例來管理用戶綁定的狀態(tài)缘回,是代碼的壞味道吆视,你應(yīng)該認(rèn)真的重新評(píng)估你的對(duì)象圖的設(shè)計(jì)典挑。
四、避免使用單例
既然單例對(duì)局部作用域的狀態(tài)有這么多的壞處啦吧,那么應(yīng)該怎樣避免使用它們呢您觉?
重溫上面的例子。既然我們的 thumbnail cache 的緩存狀態(tài)是和具體的用戶綁定的授滓,那么定義一個(gè) user 對(duì)象吧琳水。
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`@interface User : NSObject
@property (nonatomic, readonly) ThumbnailCache * thumbnailCache;
@end
@implementation User
- (instancetype)init
{
if ((self = [super init])) {
_thumbnailCache = [[ThumbnailCache alloc] init];
}
return self;
}
@end` </pre>
現(xiàn)在用一個(gè)對(duì)象來作為一個(gè)經(jīng)過認(rèn)證的用戶會(huì)話的模型類,并且可以把所有和用戶相關(guān)的狀態(tài)存儲(chǔ)在這個(gè)對(duì)象中般堆。
現(xiàn)在假設(shè)我們有一個(gè) VC 來展現(xiàn)好友列表:
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
`@interface FriendListVC : UIViewController
- (instancetype)initWithUser:(User *)user;
@end` </pre>
我們可以顯式的把經(jīng)過認(rèn)證的 user 對(duì)象作為參數(shù)傳遞給這個(gè) vc在孝。這種把依賴性傳遞給依賴對(duì)象的技術(shù)正式的叫法是依賴注入,并且它有很多優(yōu)點(diǎn):
①淮摔、對(duì)于閱讀這個(gè) FriendListVC 頭文件的人來說私沮,可以很清楚的知道它只有在有登錄用戶的情況下才會(huì)被展示。
②噩咪、這個(gè) FriendListVC 只要還在使用中顾彰,就可以強(qiáng)引用 user 對(duì)象。
<pre class="line-numbers" style="margin: 0px; padding: 0px; overflow: auto; white-space: pre !important; position: relative !important;">
Copy
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [_user.thumbnailCache cacheProfileImage:newImage forUserId:userId]; });
</pre>
這種后臺(tái)任務(wù)仍然意義重大胃碾,當(dāng)?shù)谝粋€(gè)實(shí)例失效時(shí)涨享,應(yīng)用其他地方的代碼可以創(chuàng)建和使用一個(gè)全新的 User 對(duì)象,而不會(huì)阻塞用戶交互仆百。
為了更詳細(xì)的說明一下第二點(diǎn)厕隧,讓我們畫一下在使用依賴注入之前和之后的對(duì)象圖。
- 假設(shè) FriendListVC 是當(dāng)前 window 的 root view controller俄周。使用單例時(shí)吁讨,對(duì)象圖看起來如下所示:
vc 以及自定義的 imageView,都會(huì)和 sharedThumbnailCache 產(chǎn)生交互峦朗。
當(dāng)用戶登出后建丧,清理 rootViewController 并且退出到登錄頁面:
這里的問題在于這個(gè) FriendListVC 可能仍然在執(zhí)行代碼(由于后臺(tái)操作的原因),并且可能因此仍然有一些調(diào)用被掛起到 sharedThumbnailCache 上波势。
- 使用依賴注入的對(duì)象圖:
簡單起見翎朱,假設(shè) UIApplicationDelegate 管理 User 的實(shí)例(在實(shí)際中,為了簡化 applicationDelegate 可能會(huì)把這些用戶狀態(tài)的管理工作交給另外一個(gè)對(duì)象來做)尺铣。當(dāng)展現(xiàn) FriendListVC 時(shí)拴曲,會(huì)傳遞進(jìn)去一個(gè) user 的引用。這個(gè)引用也會(huì)向下傳遞給 profileImageView×莘蓿現(xiàn)在澈灼,當(dāng)用戶登出時(shí),我們的對(duì)象圖如下所示:
這個(gè)對(duì)象圖看起來和使用單例時(shí)很像。這有什么區(qū)別叁熔?
關(guān)鍵問題是作用域委乌。在單例情況下,sharedThumbnailCache 仍然可以被程序的任意模塊訪問者疤。假如用戶快速的登錄了一個(gè)新的賬號(hào)福澡。該用戶也想看看他的好友列表,這也就意味著需要再一次的和 thumbnailCache 產(chǎn)生交互:
當(dāng)用戶登錄一個(gè)新賬號(hào)驹马,我們應(yīng)該能夠構(gòu)建并且與全新的 ThumbnailCache 交互革砸,而不需要再在銷毀老的 thumbnailCache 上花費(fèi)精力∨蠢郏基于對(duì)象管理的典型規(guī)則算利,舊的 vc 和老的 thumbnailCache 應(yīng)該能夠自己在后臺(tái)延遲被清理掉。簡而言之泳姐,我們應(yīng)該隔離用戶 A 相關(guān)聯(lián)的狀態(tài)和用戶 B 相關(guān)聯(lián)的狀態(tài):
五效拭、結(jié)論
在 iOS 開發(fā)的世界中,單例的使用是如此的普遍以至于我們有時(shí)候忘記了多年來在其他面向?qū)ο缶幊讨袑W(xué)到的教訓(xùn)胖秒。
這一切的關(guān)鍵點(diǎn)在于缎患,在面向?qū)ο缶幊讨形覀兿胍钚』勺儬顟B(tài)的作用域。但是單例卻站在了對(duì)立面阎肝,因?yàn)樗鼈兪箍勺兊臓顟B(tài)可以被程序中的任何地方訪問挤渔。下一次使用單例時(shí),希望能夠好好考慮一下使用依賴注入作為替代方案风题。