時(shí)常總結(jié)過(guò)去,方能把握今天、自信面對(duì)未來(lái)。
最近在回顧GCD及單例相關(guān)的一些知識(shí)點(diǎn),這篇文章,就重點(diǎn)說(shuō)一下單例的使用過(guò)程中需要注意的地方。
首先,咱們分析一下單例的存在意義。
- 對(duì)于某個(gè)類來(lái)說(shuō),其對(duì)象以單例的形式存在,目的是為了使整個(gè)程序中只有唯一一個(gè)實(shí)例對(duì)象,整個(gè)程序中只有一份該對(duì)象的內(nèi)存地址。外界無(wú)論有多少次的創(chuàng)建代碼,拿到的都只是最初創(chuàng)建的那個(gè)實(shí)例對(duì)象。
然后,咱們重點(diǎn)來(lái)看單例的寫法。
以新建一個(gè)Person類為例:
.h文件
#import <Foundation/Foundation.h>
@interface WSHLPerson : NSObject
+ (instancetype)sharedInstance;
@end
.m文件
@implementation WSHLPerson
static id _instance;
/**
提供類方法快速創(chuàng)建單例對(duì)象
*/
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
/**
重寫allocWithZone方法
*/
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
}
#pragma mark - NSCopying
/**
實(shí)現(xiàn)NSCopying代理方法
*/
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
return _instance;
}
@end
上面這段示例代碼,就是創(chuàng)建單例的完整過(guò)程,總共分為下面四步:
- 申明
static
實(shí)例變量 - 重寫
allocWithZone
方法 - 給外界提供快速創(chuàng)建單例的類方法
- 實(shí)現(xiàn)
NSCopying
協(xié)議下的copyWithZone
方法。
相信大多數(shù)童鞋對(duì)于前三步應(yīng)該都沒(méi)有上面問(wèn)題。
- 給實(shí)例前面加上
static
,是為了不被外界訪問(wèn)。如果沒(méi)有static
,那么外界完全可以通過(guò)sharedInstance
拿到單例對(duì)象,萬(wàn)一外界清空了這個(gè)單例對(duì)象,那么這個(gè)單例就永久性的失去意義了,而且還沒(méi)辦法二次創(chuàng)建了,因?yàn)閯?chuàng)建單例的代碼是一次性的(GCD--once
),因此此處必須加上static
關(guān)鍵詞。 - 重寫
allocWithZone
方法的目的是為了保證單例的唯一性。因?yàn)橥饨缈赡軙?huì)不用類方法來(lái)創(chuàng)建對(duì)象,而是通過(guò)常用的alloc
方法來(lái)創(chuàng)建對(duì)象。有些童鞋可能會(huì)問(wèn):為什么不是重寫alloc
方法,而是重寫allocWithZone
方法呢?這是因?yàn)?code>alloc方法其實(shí)最終還是會(huì)調(diào)用allocWithZone
方法來(lái)分配內(nèi)存,因此,這里不是重寫alloc
方法,而是重寫allocWithZone
方法。 - 提供類方法,是為了讓外界快速創(chuàng)建單例,因此幾乎所有的單例創(chuàng)建都是這樣的寫法了。
而對(duì)于第四步,可能有些童鞋就略顯陌生了,會(huì)心存疑慮:為什么要實(shí)現(xiàn)NSCopying
協(xié)議下的copyWithZone
方法呢??請(qǐng)看下面分析:
- 如果外界通過(guò)已有對(duì)象
person
,利用copy
方法[person copy]
來(lái)創(chuàng)建另一個(gè)對(duì)象時(shí),copy
方法會(huì)再調(diào)用copyWithZone
方法來(lái)創(chuàng)建對(duì)象,而如果單例所在的類內(nèi)部沒(méi)有實(shí)現(xiàn)copyWithZone
方法,那么就會(huì)發(fā)生crash,crash的原因即為:單例內(nèi)部沒(méi)有找到copyWithZone
方法。
因此,一個(gè)完整的單例的寫法,其實(shí)要將copy
方法也考慮在內(nèi),應(yīng)該考慮到外界的各種創(chuàng)建方式。
接下來(lái),咱們就說(shuō)說(shuō)在開發(fā)過(guò)程中使用單例時(shí),幾種可能存在的誤區(qū)。
誤區(qū)一:有些童鞋不通過(guò)
GCD-once
(一次性代碼)來(lái)創(chuàng)建單例,而是通過(guò)if
條件語(yǔ)句判斷實(shí)例對(duì)象是否為nil
來(lái)創(chuàng)建。
來(lái)看代碼:
static id _instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
if (!_instance) {
_instance = [super allocWithZone:zone];
}
return _instance;
}
+ (instancetype)sharedInstance {
if (!_instance) {
_instance = [[self alloc] init];
}
return _instance;
}
- (id)copyWithZone:(NSZone *)zone {
return _instance;
}
上面這段創(chuàng)建單例的代碼,并沒(méi)有使用GCD-once
(一次性代碼),而是通過(guò)判斷靜態(tài)實(shí)例是否已經(jīng)存在來(lái)創(chuàng)建單例。這樣的寫法,正常看來(lái)是沒(méi)有問(wèn)題的,但是,還是存在一定的隱患 →→ 多線程的情況
。
咱們來(lái)具體分析一下:
現(xiàn)在有兩項(xiàng)任務(wù),每項(xiàng)任務(wù)中都需要用到person單例。
這時(shí),如果需求要兩項(xiàng)任務(wù)在不同線程進(jìn)行,那么必然會(huì)用到多線程來(lái)處理了。而此時(shí)如果用上面的方法來(lái)創(chuàng)建單例,那么就有可能會(huì)出現(xiàn)隱患了:
當(dāng)線程A
來(lái)到 allocWithZone
方法時(shí),發(fā)現(xiàn)單例是nil
,所以,就會(huì)準(zhǔn)備執(zhí)行 _instance = [super allocWithZone:zone]
,注意,這里說(shuō)的是“準(zhǔn)備執(zhí)行”,而正好在這個(gè)時(shí)候,線程B
也來(lái)到了allocWithZone
方法,判斷發(fā)現(xiàn)單例還是nil
,所以,線程B
也會(huì)開始執(zhí)行 _instance = [super allocWithZone:zone]
,這樣一來(lái),就會(huì)創(chuàng)建出兩個(gè)單例對(duì)象(二者內(nèi)存地址不一樣),這就失去了單例存在的意義,進(jìn)而就會(huì)引發(fā)一連串類似于數(shù)據(jù)對(duì)應(yīng)不一致的問(wèn)題了。。
這樣的問(wèn)題是有可能發(fā)生的,而且不好重現(xiàn),更不好定位問(wèn)題的癥結(jié)所在,所以會(huì)很頭疼。。。。
因此,創(chuàng)建單例的方式,一定要通過(guò)GCD-once
(一次性代碼)來(lái)創(chuàng)建單例對(duì)象,就會(huì)避免這樣的問(wèn)題。
- 首先,
GCD-once
(一次性代碼)內(nèi)部是線程安全的,這就已經(jīng)排除了隱患。 - 其次,
GCD-once
作為全局的一次性代碼,無(wú)論是否為多線程,只要有一條線程已經(jīng)進(jìn)入到了GCD-once
的內(nèi)部代碼,那么其他線程即便是原本需要執(zhí)行該代碼,也不會(huì)執(zhí)行了。
分析這個(gè)誤區(qū),一是希望有這種誤區(qū)的童鞋盡早認(rèn)清其隱患,二是希望使用單例的童鞋們能對(duì)單例的正確寫法有更深刻的理解,而不僅僅是停留在copy代碼的層面。
誤區(qū)二:有些童鞋為了簡(jiǎn)化代碼,使用繼承的方式來(lái)創(chuàng)建新的單例類。
由于單例的創(chuàng)建代碼都是一樣的,因此,有些童鞋為了避免多次重復(fù)編寫創(chuàng)建單例的代碼,就想通過(guò)繼承的方式來(lái)省去創(chuàng)建單例的代碼。于是,就會(huì)在寫好一個(gè)單例后,讓其他單例都繼承自這個(gè)類。
比如說(shuō),文章一開始創(chuàng)建單例的實(shí)例代碼中,創(chuàng)建了一個(gè)WSHLPerson
類的單例,此時(shí),又有WSHLChinese
和WSHLAmerican
兩個(gè)類也是需要運(yùn)用單例模式,于是,有些童鞋就會(huì)直接讓這兩個(gè)類繼承自WSHLPerson
,然后在這兩個(gè)類中什么都不做。因?yàn)榉凑割愔刑峁┝藙?chuàng)建單例的 sharedInstance
方法。
下面,咱們就通過(guò)實(shí)際代碼測(cè)試,來(lái)看一下這樣做到底可不可以。
- 創(chuàng)建
WSHLChinese
和WSHLAmerican
,讓這兩個(gè)類都繼承自WSHLPerson
。 - 在控制器中引入創(chuàng)建的
WSHLChinese
和WSHLAmerican
這兩個(gè)類的.h文件,然后在控制器的touchesBegan
方法中,分別創(chuàng)建WSHLChinese
和WSHLAmerican
單例對(duì)象。 - 打印一下這兩個(gè)單例對(duì)象。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"----%@----",[WSHLChinese sharedInstance]);
NSLog(@"----%@----",[WSHLAmerican sharedInstance]);
}
咱們一起來(lái)看打印結(jié)果:
2018-12-12 15:23:11.028602+0800 TestGCD[3347:122815] ----<WSHLChinese: 0x604000007b50>----
2018-12-12 15:23:11.029014+0800 TestGCD[3347:122815] ----<WSHLChinese: 0x604000007b50>----
輸入結(jié)果顯示,創(chuàng)建的兩個(gè)單例都是屬于WSHLChinese
類。為什么會(huì)這樣呢?
因?yàn)閯?chuàng)建單例的代碼是通過(guò)GCD-once
(一次性代碼)完成的,整個(gè)程序只會(huì)執(zhí)行一次這段代碼,因此,由于是創(chuàng)建WSHLChinese
單例的代碼在前,所以當(dāng)WSHLChinese
這個(gè)單例創(chuàng)建完成后,單例(全局的static
)已經(jīng)存在了,那么后續(xù)的WSHLAmerican
創(chuàng)建單例時(shí),就不會(huì)再執(zhí)行GCD-once
代碼了,而是直接返回創(chuàng)建好的單例對(duì)象了,也就是WSHLChinese
單例。同樣的,如果將上面兩行代碼互換位置,先創(chuàng)建WSHLAmerican
單例,后創(chuàng)建WSHLChinese
單例,那么結(jié)果就會(huì)是兩個(gè)單例都是WSHLAmerican
類。
因此,在實(shí)際開發(fā)中使用單例,切勿用繼承的方式來(lái)省去創(chuàng)建單例的代碼。
那么,既然創(chuàng)建單例的代碼都是一樣的,如何能夠做到不重復(fù)編寫呢?答案:將單例的創(chuàng)建過(guò)程封裝在一個(gè)宏定義里面。
新建一個(gè)繼承自NSObject
的類,將.m
文件delete,.h
文件中的所有預(yù)備代碼全部delete。然后定義兩個(gè)宏,分別對(duì)應(yīng)單例所在類的.h
和.m
文件中的代碼:
/**
對(duì)應(yīng).h文件
*/
#define WSHLSingletonH + (instancetype)sharedInstance;
/**
對(duì)應(yīng).m文件
*/
#define WSHLSingletonM \
\
static id _instance;\
\
+ (instancetype)sharedInstance {\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [[self alloc] init];\
});\
return _instance;\
}\
\
+ (instancetype)allocWithZone:(struct _NSZone *)zone {\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
- (nonnull id)copyWithZone:(nullable NSZone *)zone {\
return _instance;\
}
這樣的話,以后新建單例,直接在.h
、.m
文件中加入兩個(gè)宏就可以了。下面以Car為例,示范一下:
.h
文件中,引入宏定義所在的.h
文件,然后寫上 .h
對(duì)應(yīng)的宏定義
#import <Foundation/Foundation.h>
#import "WSHLSingleton.h" // 單例宏定義對(duì)應(yīng)的頭文件
@interface WSHLCar : NSObject
WSHLSingletonH
@end
.m
文件中,寫上 .m
對(duì)應(yīng)的宏定義
#import "WSHLCar.h"
@implementation WSHLCar
WSHLSingletonM
@end
這樣就創(chuàng)建好一個(gè)單例類了,使用起來(lái)就很方便了:
NSLog(@"%@",[WSHLCar sharedInstance]);
當(dāng)然了,可能有些童鞋創(chuàng)建單例的方法名不喜歡用通用的sharedInstance
,而是想用sharedCar
、sharedPerson
等等,這也很簡(jiǎn)單,只要在宏定義里面加上個(gè)參數(shù)即可,這里就不再羅列代碼了,有興趣的童鞋自行搞一下好啦。