今天給同學們講解一下單例模式在iOS開發(fā)中的使用以及單例模式的相關優(yōu)缺點,那么廢話不多說,直接上代碼~
- 單例模式介紹和使用場景
- 為什么選擇單例模式?
- 實現(xiàn)單例模式思路分析(
核心&掌握
) - 通過@synchronized/dispatch_once 實現(xiàn)單例(
掌握
) - 單例為什么不能通過繼承來實現(xiàn)(
掌握
) - 通過宏定義來寫一個MRC/ARC環(huán)境下的單例(
掌握
) - 單例模式的優(yōu)缺點(
掌握
) - 單例模式誤區(qū)(
了解
)
單例模式
- 單例模式的作用
可以保證在程序運行過程,一個類只有一個實例,而且該實例易于供外界訪問
從而方便地控制了實例個數(shù)判呕,并節(jié)約系統(tǒng)資源 - 單例模式的使用場合
在整個應用程序中,共享一份資源(這份資源只需要創(chuàng)建初始化1次) - 什么時候選擇單例模式呢已卸?(
重點
)-
官方說法
一個類必須只有一個對象佛玄。客戶端必須通過一個眾所周知的入口訪問這個對象累澡。
這個唯一的對象需要擴展的時候梦抢,只能通過子類化的方式±⒂矗客戶端的代碼能夠不需要任何修改就能夠使用擴展后的對象奥吩。 -
個人理解
上面的官方說法哼蛆,聽起來一頭霧水。我的理解是這樣的霞赫。
在建模的時候腮介,如果這個東西確實只需要一個對象,多余的對象都是無意義的端衰,那么就考慮用單例模式叠洗。比如定位管理(CLLocationManager),硬件設備就只有一個旅东,弄再多的邏輯對象意義不大灭抑。
-
實現(xiàn)單例模式思路分析(核心
)
- 1> 首先我們知道單例模式就是保障在整個應用程序中,一個類只有一個實例抵代,而我們知道創(chuàng)建對象 通過調(diào)用alloc init 方法初始化而alloc方法是用來分配內(nèi)存空間 所以我們就是攔截alloc方法保證只分配一次內(nèi)存空間腾节。(
核心思路出發(fā)點
) -
2> 我們通過查閱官方Api文檔如下
- 3> 那么我們就從allocWithZone方法入手但是我們?nèi)绾伪WC只創(chuàng)建一個實例對象呢?尤其在多線程的情況下荤牍,那么有同學就想到了加鎖案腺,iOS中控制多線程的方式有很多,可以使用NSLock康吵,也可以用@synchronized等各種線程同步的技術劈榨,代碼如下。(
掌握
)
// 該類內(nèi)的全局變量涎才,外界就不能訪問和修改鞋既,變量名取_book是為了和該類的其余成員屬性區(qū)分開!牛逼的大神都這么寫 so 建議這么寫耍铜。
static ZZBook *_book;
@implementation ZZBook
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (_book == nil) {
_book = [super allocWithZone:zone];
}
}
return _book;
}
通過查看log我們發(fā)現(xiàn)確實做到了無論創(chuàng)建多少次都是同一個內(nèi)存地址。
- 4> OC的內(nèi)部機制里有一種更加高效的方式跌前,那就是dispatch_once棕兼。性能相差好幾倍,好幾十倍抵乓。代碼如下!關于性能的比對伴挚,大神們做過實驗和分析。請參考http://blog.jimmyis.in/dispatch_once/灾炭。(
掌握
)
// 該類內(nèi)的全局變量茎芋,外界就不能訪問和修改,變量名取_person是為了和該類的其余成員屬性區(qū)分開蜈出!牛逼的大神都這么寫 so 建議這么寫田弥。
static ZZPerson *_person;
@implementation ZZPerson
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_person = [super allocWithZone:zone];
});
return _person;
}
- 5> 到這里我們實現(xiàn)了通過allocWithZone通過加鎖只分配一次內(nèi)存空間但是我們通過觀察系統(tǒng)的單例例如UIApplication / NSUserDefaults 等都會提供一個快捷的類方法訪問那么我們參照系統(tǒng)的做法,代碼如下铡原。(
掌握
)
+ (instancetype)sharedBook
{
@synchronized(self) {
if (_book == nil) {
_book = [[self alloc] init];
}
}
return _book;
}
- 6> Objective-C中構造方法不像別的語言如C++偷厦,java可以隱藏構造方法商叹,實則是公開的!由Objective-C的一些特性可以知道只泼,在對象創(chuàng)建的時候剖笙,無論是alloc還是new,都會調(diào)用到 allocWithZone方法请唱。在通過拷貝的時候創(chuàng)建對象時弥咪,會調(diào)用到-(id)copyWithZone:(NSZone *)zone,-(id)mutableCopyWithZone:(NSZone *)zone方法十绑。因此酪夷,可以重寫這些方法,讓創(chuàng)建的對象唯一孽惰。代碼如下M砹搿(
掌握
)
//
// ZZBook.m
// 8-多線程技術
//
// Created by Jordan zhou on 2018/11/15.
// Copyright ? 2018年 Jordan zhou. All rights reserved.
//
#import "ZZBook.h"
@interface ZZBook()<NSCopying,NSMutableCopying>
@end
// 該類內(nèi)的全局變量,外界就不能訪問和修改勋功,變量名取_book是為了和該類的其余成員屬性區(qū)分開坦报!牛逼的大神都這么寫 so 建議這么寫。
static ZZBook *_book;
@implementation ZZBook
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (_book == nil) {
_book = [super allocWithZone:zone];
}
}
return _book;
}
+ (instancetype)sharedBook
{
@synchronized(self) {
if (_book == nil) {
_book = [[self alloc] init];
}
}
return _book;
}
- (id)copyWithZone:(NSZone *)zone
{
return _book;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return _book;
}
@end
- 7> 補充第5點我們可以通過重寫方法狂鞋,讓創(chuàng)建的對象唯一片择,我們同樣也可以通過編譯器告訴外面,alloc骚揍,new字管,copy,mutableCopy方法不可以直接調(diào)用信不。否則編譯不過嘲叔。代碼如下!
+(instancetype) alloc __attribute__((unavailable("call sharedBook instead")));
+(instancetype) new __attribute__((unavailable("call sharedBook instead")));
-(instancetype) copy __attribute__((unavailable("call sharedBook instead")));
-(instancetype) mutableCopy __attribute__((unavailable("call sharedBook instead")));
當外部通過如上方法創(chuàng)建時會直接報錯如下
- 8> 通過dispatch_once實現(xiàn)單例的代碼如下3榛睢(
掌握
)
//
// ZZPerson.m
// 8-多線程技術
//
// Created by Jordan zhou on 2018/11/15.
// Copyright ? 2018年 Jordan zhou. All rights reserved.
//
#import "ZZPerson.h"
@interface ZZPerson()<NSCopying,NSMutableCopying>
@end
// 該類內(nèi)的全局變量硫戈,外界就不能訪問和修改,變量名取_person是為了和該類的其余成員屬性區(qū)分開下硕!牛逼的大神都這么寫 so 建議這么寫丁逝。
static ZZPerson *_person;
@implementation ZZPerson
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_person = [super allocWithZone:zone];
});
return _person;
}
+ (instancetype)sharedPerson
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_person = [[self alloc] init];
});
return _person;
}
- (id)copyWithZone:(NSZone *)zone
{
return _person;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return _person;
}
@end
-
9> 通過對比@synchronized或者dispatch_once實現(xiàn)單例代碼,每個類中會發(fā)現(xiàn)寫的代碼都是完全相同的 除了命名的參數(shù)不同以及方法名不一樣梭姓,如下圖霜幼!
- 10> 承接第9點,那么有人肯定想到那可不可以用繼承呢誉尖?我們測試通過繼承的方式打印如下代碼罪既!
#pragma mark - 測試通過繼承來實現(xiàn)單例
- (void)singleton3
{
ZZPerson *p1 = [[ZZPerson alloc] init];
ZZPerson *p2 = [ZZPerson sharedInstance];
ZZBook *b1 = [[ZZBook alloc] init];
ZZBook *b2 = [ZZBook sharedInstance];
NSLog(@"%@ - %@ - %@ - %@",p1,p2,b1,b2);
}
結果如下圖: 通過打印我們發(fā)現(xiàn)書的類型也變成人了 那是因為一次性代碼在程序運行過創(chuàng)建一次ZZPerson比ZZBook先創(chuàng)建那么_instance的值永遠為ZZPerson類型所以是不能通過繼承來實現(xiàn)單例的
- 11> 爭取方式通過宏定義來寫以后要用直接拖走這個宏就可以!而單例模式在ARC\MRC環(huán)境下的寫法有所不同,需要編寫2套不同的代碼萝衩!可以用宏判斷是否為ARC環(huán)境回挽!
#if __has_feature(objc_arc)
// ARC
#else
// MRC
#endif
編寫代碼如下!
// name是外部傳遞的參數(shù) ##是拼接符 用來拼接參數(shù)
// .h文件
#define ZZSingletonH(name) + (instancetype)shared##name;
// 如何定義一個宏表示后面都屬于這個宏 +上" \" 即可表示后面所以的東西都屬于這個宏
// .m文件
#if __has_feature(objc_arc) // 是ARC
#define ZZSingletonM(name) \
static id _instance; \
\
+ (instancetype)allocWithZone:(struct _NSZone *)zone \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [super allocWithZone:zone]; \
}); \
return _instance; \
} \
\
+ (instancetype)shared##name \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [[self alloc] init]; \
}); \
return _instance; \
} \
\
- (id)copyWithZone:(NSZone *)zone \
{ \
return _instance; \
} \
\
- (id)mutableCopyWithZone:(NSZone *)zone \
{ \
return _instance; \
}
#else // 不是ARC
#define ZZSingletonM(name) \
static id _instance; \
+ (instancetype)allocWithZone:(struct _NSZone *)zone \
{ \
if (_instance == nil) { \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [super allocWithZone:zone]; \
}); \
} \
return _instance; \
} \
\
+ (instancetype)shared##name \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [[self alloc] init]; \
}); \
return _instance; \
} \
\
- (oneway void)release \
{ \
\
} \
\
- (id)retain \
{ \
return self; \
} \
\
- (NSUInteger)retainCount \
{ \
return 1; \
} \
+ (id)copyWithZone:(struct _NSZone *)zone \
{ \
return _instance; \
} \
\
+ (id)mutableCopyWithZone:(struct _NSZone *)zone \
{ \
return _instance; \
}
#endif
單例模式的優(yōu)缺點(掌握
)
使用簡單猩谊、延時求值千劈、易于跨模塊
- 內(nèi)存占用與運行時間
對比使用單例模式和非單例模式的例子,在內(nèi)存占用與運行時間存在以下差距:- 單例模式:單例模式每次獲取實例時都會先進行判斷,看該實例是否存在——如果存在,則返回疑务;否則,則創(chuàng)建實例喜滨。因此,會浪費一些判斷的時間撤防。但是虽风,如果一直沒有人使用這個實例的話,那么就不會創(chuàng)建實例寄月,節(jié)約了內(nèi)存空間辜膝。
- 非單例模式:當類加載的時候就會創(chuàng)建類的實例,不管你是否使用它漾肮。然后當每次調(diào)用的時候就不需要判斷該實例是否存在了厂抖,節(jié)省了運行的時間。但是如果該實例沒有使用的話克懊,就浪費了內(nèi)存忱辅。
- 線程的安全性
- 從線程的安全性上來講,不加同步的單例模式是不安全的谭溉。比如墙懂,有兩個線程,一個是線程A夜只,另外一個是線程B垒在,如果它們同時調(diào)用某一個方法,那就可能會導致并發(fā)問題扔亥。在這種情況下,會創(chuàng)建出兩個實例來谈为,也就是單例的控制在并發(fā)情況下失效了旅挤。
- 非單例模式是線程安全的,因為程序保證只加載一次伞鲫,在加載的時候不會發(fā)生并發(fā)情況粘茄。
- 單例模式如果要實現(xiàn)線程安全,只需要加上synchronized即可。但是這樣一來柒瓣,就會減低整個程序的訪問速度儒搭,而且每次都要判斷,比較麻煩芙贫。
- 雙重檢查加鎖:為了解決如上的繁瑣問題搂鲫,可以使用“雙重檢查加鎖”的方式來實現(xiàn),這樣磺平,就可以既實現(xiàn)線程安全魂仍,又能使得程序性能不受太大的影響。
- 單例模式會阻止其它對象實例化其自己的對象的副本拣挪,從而確保所有對象都訪問唯一實例擦酌。
- 因為單例模式的類控制了實例化的過程,所以類可以更加靈活修改實例化過程菠劝。
單例模式誤區(qū)(了解
)
- 內(nèi)存問題
- 單例模式實際上延長了對象的生命周期赊舶。那么就存在內(nèi)存問題。因為這個對象在程序的整個生命都存在赶诊。所以當這個單例比較大的時候笼平,總是hold住那么多內(nèi)存,就需要考慮這件事了甫何。
- 另外出吹,可能單例本身并不大,但是它如果強引用了另外的比較大的對象辙喂,也算是一個問題捶牢。別的對象因為單例對象不釋放而不釋放。
當然這個問題也有一定的辦法巍耗。比如對于一些可以重新加載的對象秋麸,在需要的時候加載,用完之后炬太,單例對象就不再強引用灸蟆,從而把原先hold住的對象釋放掉。下次需要再加載回來亲族。
- 循環(huán)依賴問題
- 在開發(fā)過程中炒考,單例對象可能有一些屬性,一般會放在init的時候創(chuàng)建和初始化霎迫。這樣斋枢,比如如果單例A的m屬性依賴于單例B,單例B的屬性n依賴于單例A知给,初始化的時候就會出現(xiàn)死循環(huán)依賴瓤帚。死在dispatch_once里描姚。
- 對于這種情況,最好的設計是在單例設計的時候戈次,初始化的內(nèi)容不要依賴于其他對象轩勘。如果實在要依賴,就不要讓它形成環(huán)怯邪。實在會形成環(huán)或者無法控制绊寻,就采用異步初始化的方式。先過去擎颖,內(nèi)容以后再填榛斯。內(nèi)部需要做個標識,標識這個單例在造出來之后搂捧,不能立刻使用或者完整使用驮俗。