這是一篇objc上的文章躺坟,解了我很多的關(guān)于單例的困惑阅懦,原文地址 中文翻譯地址
1.單例介紹
單例是Cocoa中被廣泛使用的設(shè)計模式之一蝗砾, 比如系統(tǒng)的UIApplication和NSFileManager等汇荐。
+(instancetype)sharedInstance{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once,^{
sharedInstance = [[self alloc]init];
});
return sharedInstance;
}
單例的含義是:一個應(yīng)用程序中日熬,只有一個該類的實例棍厌。它在第一次被訪問的時候創(chuàng)建,在應(yīng)用結(jié)束的時候結(jié)束竖席。
2. 單例的問題1:全局狀態(tài)
大多數(shù)人認(rèn)為全局狀態(tài)是不好的行為耘纱,太多的狀態(tài)難以理解,難以調(diào)試毕荐。
首先我們看一個例子1
@implementation SPMath
{
NSInteger _a;
NSInteger _b;
}
-(NSInteger)computeSum{
return _a + _b;
}
//例子1 有兩個問題:
- computeSum函數(shù)沒有顯式的聲明它依賴 _a 和 _b 束析。 我們需要看這個函數(shù)具體的實現(xiàn)才能明白這個函數(shù)依賴那些變量。 隱藏依賴不好东跪。
- 當(dāng)為調(diào)用computeSum做準(zhǔn)備而修改_a 和 _b的數(shù)值的時候畸陡,我們需要保證這些修改不會影響其他依賴于這兩個變量的代碼的正確性鹰溜,而這在多線程環(huán)境是尤其困難的。
下邊是例子2:
+(NSInteger)computeSumOf:(NSInteger)a plus:(NSInteger)b{
return a+b
}
例子2中丁恭,我們顯式的聲明了依賴曹动,我們不需要為了調(diào)用這個方法而去改變實例變量的狀態(tài)。
這個例子和單例的關(guān)系: 單例是全局狀態(tài)牲览,它可以被使用在任何地方墓陈,而不需要顯式的聲明依賴。我們在代碼的任何地方都可以調(diào)用[Singleton sharedInstance]來和這個單例交互第献,同時它也會影響到程序其他用到該單例地方的代碼贡必。
例子3:單例的全局狀態(tài)對其他地方代碼的影響
@interface SPSingleton : NSObject
+ (instancetype)sharedInstance;
- (NSUInteger)badMutableState;
- (void)setBadMutableState:(NSUInteger)badMutableState;
@end
@implementation SPConsumerA
- (void)someMethod
{
if ([[SPSingleton sharedInstance] badMutableState] ==0 ) {
//do somethingA
}else{
//do somethingB
}
}
@end
@implementation SPConsumerB
- (void)someOtherMethod
{
[[SPSingleton sharedInstance] setBadMutableState:0];
}
@end
// 上邊的例子中,SPConsumerA和SPConsumerB是兩個完全獨(dú)立的模塊庸毫,但是呢仔拟,SPConsumerB卻可以通過使用單例,來影響到SPConsumerA的行為,這種情況只能發(fā)生在B模塊顯式的引用A模塊來表明二者之間關(guān)系飒赃。 但是這里送單例利花,導(dǎo)致隱式地兩個不相關(guān)的模塊建立了耦合。
例子4(因為我對測試這塊沒掌握好载佳,這里不是很了解)
@interface SPURLCache
+ (SPCache *)sharedURLCache;
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
@end
我們寫了一個網(wǎng)頁查看器炒事,并構(gòu)建了一個URLCache。然后我們寫了3個測試用例蔫慧,沒有網(wǎng)絡(luò)的測試用例挠乳,失敗的測試用例,成功的測試用例姑躲。 過一段時間睡扬,當(dāng)有人改變了測試用例執(zhí)行順序的時候,發(fā)現(xiàn)測試用例不能正常運(yùn)行了肋联。
處理成功的那個測試用例首先被運(yùn)行威蕉,然后再運(yùn)行其他兩個。處理錯誤的那兩個測試用例現(xiàn)在竟然成功了橄仍,和預(yù)期不一樣韧涨,因為 URL cache 這個單例把不同測試用例之間的 response 緩存起來了。
// 這里的結(jié)論是: 持久化的狀態(tài)是單元測試的敵人侮繁。因為單元測試在各個測試用例相互獨(dú)立的情況下才有效虑粥。如果狀態(tài)從一個用例傳遞到另一個,這樣就和測試用例的執(zhí)行順序有關(guān)系了宪哩。
3. 單例的問題2: 對象的生命周期娩贷。
當(dāng)我們在程序中添加一個單例時,很容易認(rèn)為锁孟,永遠(yuǎn)只會有一個實例彬祖。但是茁瘦,在很多iOS代碼中,這種假定很可能被打破储笑。下面是一個例子:當(dāng)單例的生命周期不是整個應(yīng)用的時候甜熔,使用單例會不恰當(dāng)。
//我們有一個應(yīng)用程序突倍,應(yīng)用的用戶可以看好友列表腔稀。有一個thumbnailCache單例,來在設(shè)備上緩存這些好友的圖片信息羽历。
例子5:
//使用dispatch_once創(chuàng)建單例
@interface SPThumbnailCache : NSObject
+ (instancetype)sharedThumbnailCache;
- (void)cacheProfileImage:(NSData *)imageData forUserId:(NSString *)userId;
@end
//考慮這樣一種情形焊虏,當(dāng)我們有一天實現(xiàn)注銷功能,我們退出登錄用戶1秕磷,然后使用新的賬號用戶2登錄诵闭。這時候我們需要清除之前緩存的信息。如果此時跳夭,單例在子線程執(zhí)行緩存圖片任務(wù)涂圆,那么因為單例一直存在们镜,我們無法取消單例繼續(xù)用戶1的好友圖片币叹。 而此時,我們登錄的是用戶2模狭。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[SPThumbnailCache sharedThumbnailCache] cacheProfileImage:newImage forUserId:userId];
});
結(jié)論:單例應(yīng)該只用來保存全局的狀態(tài)颈抚,并且不能和任何作用域綁定。如果這些狀態(tài)的作用域比一個完整的應(yīng)用程序的生命周期要短嚼鹉,那么這個狀態(tài)就不應(yīng)該使用單例來管理贩汉。用一個單例來管理用戶綁定的狀態(tài),是不恰當(dāng)?shù)脑O(shè)計模式锚赤。
4. 如何避免使用單例:
依賴注入:我們可以顯式的把對象作為參數(shù)傳遞給依賴對象匹舞。這種技術(shù)叫做依賴注入。 通過依賴注入的方式线脚,避免使用單例赐稽。