單例模式大概是設計模式中最簡單的一個。本來沒什么好說的,但是實踐過程中還是有一些坑。所以本文小結一下在iOS開發中的單例模式。
一、 什么是單例模式
按照四人幫(GOF)教科書的說法,標準定義是這樣的:
Ensures a class has only one instance, and provide a global point of access to it.
保證一個類只有一個實例,并且提供一個全局的訪問入口訪問這個實例。
然后,類圖是這個樣子的:
什么時候選擇單例模式呢?
- 一個類必須只有一個對象。客戶端必須通過一個眾所周知的入口訪問這個對象。
- 這個唯一的對象需要擴展的時候,只能通過子類化的方式。客戶端的代碼能夠不需要任何修改就能夠使用擴展后的對象。
上面的官方說法,聽起來一頭霧水。我的理解是這樣的。
在建模的時候,如果這個東西確實只需要一個對象,多余的對象都是無意義的,那么就考慮用單例模式。比如定位管理(CLLocationManager),硬件設備就只有一個,弄再多的邏輯對象意義不大。所以就會考慮用單例。
二、 如何實現基本的單例模式?
那么,我們就用Objective-C來實現一下單例模式吧。
要實現比較好的訪問,我們就會想到用工廠方法創建對象,提供統一的創建方法的地方給外部使用。要實現僅有一個對象,就會想到用一個全局的東西保存這個對象,然后在創建對象的工廠方法中判斷一下,如果對象存在,那么就返回該對象。如果不存在,就造一個返回出去。
于是,基本的單例實現就這樣了:
DJSingleton * g_instance_dj_singleton = nil ;
+ (DJSingleton *)shareInstance{
if (g_instance_dj_singleton == nil) {
g_instance_dj_singleton = [[DJSingleton alloc] init];
}
return (DJSingleton *)g_instance_dj_singleton;
}
看起來不錯。不過這個全局的變量 g_instance_dj_singleton
有個缺點,就是外面的人隨便可以改,為了隔離外部修改,可以設置成靜態變量,就是這樣子:
1 + (DJSingleton *)shareInstance{
2 static DJSingleton * s_instance_dj_singleton = nil ;
3 if (s_instance_dj_singleton == nil) {
4 s_instance_dj_singleton = [[DJSingleton alloc] init];
5 }
6 return (DJSingleton *)s_instance_dj_singleton;
7 }
單例的核心思想算是實現了。
三、 多線程怎么辦?
雖然核心思想實現了,但是依舊不完美。考慮下多線程的情況。即多個線程同時訪問這個工廠方法,能夠總是保證只創建一個實例對象么?
顯然上面的方式是有問題的。比如第一個線程執行到第4行但是還沒有進行賦值操作,第二個線程執行第三行。此時判斷對象依舊為nil,第二個線程也能往下執行到創建對象操作的第4行。從而創建了多個對象。
那么,如何保證多線程下依舊能夠只創建一個呢?這里面的核心思路,是要保證s_instance_dj_singleton
這個臨界資源的訪問(讀取和賦值)。
iOS下控制多線程的方式有很多,可以使用NSLock
,可以@synchronized
等各種線程同步的技術。于是,我們的單例代碼變成了這樣:
1 + (DJSingleton *)shareInstance{
2 static DJSingleton * s_instance_dj_singleton = nil ;
3 @synchronized(self) {
4 if (s_instance_dj_singleton == nil) {
5 s_instance_dj_singleton = [[DJSingleton alloc] init];
6 }
7 }
8 return (DJSingleton *)s_instance_dj_singleton;
9 }
看起來多線程沒啥問題了了。不過我們可以做的更好。OC的內部機制里有一種更加高效的方式,那就是dispatch_once。性能相差好幾倍,好幾十倍。關于性能的比對,大神們做過實驗和分析。請參考這里。
于是,我們的單例變成了這個樣子:
+ (DJSingleton *)shareInstance{
static DJSingleton * s_instance_dj_singleton = nil ;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (s_instance_dj_manager == nil) {
s_instance_dj_manager = [[DJSingleton alloc] init];
}
});
return (DJSingleton *)s_instance_dj_singleton;
}
四、Objective-C的坑
看起來很完美了。可是Objective-C畢竟是Objective-C。別的語言,諸如C++,java,構造方法可以隱藏。Objective-C中的方法,實際上都是公開的,雖然我們提供了一個方便的工廠方法的訪問入口,但是里面的alloc方法依舊是可見的,可以調用到的。也就是說,雖然你給了我一個工廠方法,調皮的小伙伴可能依舊會使用alloc的方式創建對象。這樣會導致外面使用的時候,依舊可能創建多個實例。
關于這個事情的處理,可以分為兩派。一個是冷酷派,技術上實現無論你怎么調用,我都給你同一個單例對象;一個是溫柔派,是從編譯器上給調皮的小伙伴提示,你不能這么造對象,溫柔的指出有問題,但不強制約束。
1. 冷酷派的實現
冷酷派的實現從OC的對象創建角度出發,就是把創建對象的各種入口給封死了。alloc,copy等等,無論是采用哪種方式創建,我都保證給出的對象是同一個。
由Objective-C的一些特性可以知道,在對象創建的時候,無論是alloc還是new,都會調用到 allocWithZone
方法。在通過拷貝的時候創建對象時,會調用到-(id)copyWithZone:(NSZone *)zone
,-(id)mutableCopyWithZone:(NSZone *)zone
方法。因此,可以重寫這些方法,讓創建的對象唯一。
+(id)allocWithZone:(NSZone *)zone{
return [DJSingleton sharedInstance];
}
+(DJSingleton *) sharedInstance{
static DJSingleton * s_instance_dj_singleton = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
s_instance_dj_singleton = [[super allocWithZone:nil] init];
});
return s_instance_dj_singleton;
}
-(id)copyWithZone:(NSZone *)zone{
return [DJSingleton sharedInstance];
}
-(id)mutableCopyWithZone:(NSZone *)zone{
return [DJSingleton sharedInstance];
}
2. 溫柔派的實現
溫柔派就直接告訴外面,alloc,new,copy,mutableCopy方法不可以直接調用。否則編譯不過。
+(instancetype) alloc __attribute__((unavailable("call sharedInstance instead")));
+(instancetype) new __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) copy __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));
我個人的話比較喜歡采用溫柔派的實現。不需要這么多復雜的實現。也讓使用方有比較明確的概念這個是個單例,不要調皮。對于一般的業務場景是足夠了的。
五、 可不可以再方便點?
可以。
大神們把單例模式的各種套路封裝成了宏。這樣使用的時候,就不需要每個類都手動寫一遍里面的重復代碼了。省去了敲代碼的時間。
以溫柔派的為例,大概是這樣子的。
#define DJ_SINGLETON_DEF(_type_) + (_type_ *)sharedInstance;\
+(instancetype) alloc __attribute__((unavailable("call sharedInstance instead")));\
+(instancetype) new __attribute__((unavailable("call sharedInstance instead")));\
-(instancetype) copy __attribute__((unavailable("call sharedInstance instead")));\
-(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));\
#define DJ_SINGLETON_IMP(_type_) + (_type_ *)sharedInstance{\
static _type_ *theSharedInstance = nil;\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
theSharedInstance = [[super alloc] init];\
});\
return theSharedInstance;\
}
那么,在定義和實現的時候就很簡單了:
@interface DJSingleton : NSObject
DJ_SINGLETON_DEF(DJSingleton);
@end
@implementation DJSingleton
DJ_SINGLETON_IMP(DJSingleton);
@end
六、 單例模式潛在的問題
1. 內存問題
單例模式實際上延長了對象的生命周期。那么就存在內存問題。因為這個對象在程序的整個生命都存在。所以當這個單例比較大的時候,總是hold住那么多內存,就需要考慮這件事了。另外,可能單例本身并不大,但是它如果強引用了另外的比較大的對象,也算是一個問題。別的對象因為單例對象不釋放而不釋放。
當然這個問題也有一定的辦法。比如對于一些可以重新加載的對象,在需要的時候加載,用完之后,單例對象就不再強引用,從而把原先hold住的對象釋放掉。下次需要再加載回來。
2. 循環依賴問題
在開發過程中,單例對象可能有一些屬性,一般會放在init的時候創建和初始化。這樣,比如如果單例A的m屬性依賴于單例B,單例B的屬性n依賴于單例A,初始化的時候就會出現死循環依賴。死在dispatch_once里。
@interface DJSingletonA : NSObject
DJ_SINGLETON_DEF(DJSingletonA);
@end
@interface DJSingletonB : NSObject
DJ_SINGLETON_DEF(DJSingletonB);
@end
@interface DJSingletonA()
@property(nonatomic, strong) id someObj;
@end
@implementation DJSingletonA
DJ_SINGLETON_IMP(DJSingletonA);
-(id)init{
if (self = [super init]) {
_someObj = [DJSingletonB sharedInstance];
}
return self;
}
@end
@interface DJSingletonB()
@property(nonatomic, strong) id someObj;
@end
@implementation DJSingletonB
DJ_SINGLETON_IMP(DJSingletonB);
-(id)init{
if (self = [super init]) {
_someObj = [DJSingletonA sharedInstance];
}
return self;
}
@end
//---------------------------------------
DJSingletonA * s1 = [DJSingletonA sharedInstance];
對于這種情況,最好的設計是在單例設計的時候,初始化的內容不要依賴于其他對象。如果實在要依賴,就不要讓它形成環。實在會形成環或者無法控制,就采用異步初始化的方式。先過去,內容以后再填。內部需要做個標識,標識這個單例在造出來之后,不能立刻使用或者完整使用。
七、參考資料:
1. Design Patterns -- GOF
2. Pro Objective-C Design Patterns for iOS -- Carlo Chung
3. GCD 中 dispatch_once 的性能與實現