時(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)看單例的寫(xiě)法命雀。
以新建一個(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;
}
/**
重寫(xiě)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í)例變量 - 重寫(xiě)
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)鍵詞。 - 重寫(xiě)
allocWithZone
方法的目的是為了保證單例的唯一性井氢。因?yàn)橥饨缈赡軙?huì)不用類方法來(lái)創(chuàng)建對(duì)象弦追,而是通過(guò)常用的alloc
方法來(lái)創(chuàng)建對(duì)象。有些童鞋可能會(huì)問(wèn):為什么不是重寫(xiě)alloc
方法花竞,而是重寫(xiě)allocWithZone
方法呢劲件?這是因?yàn)?code>alloc方法其實(shí)最終還是會(huì)調(diào)用allocWithZone
方法來(lái)分配內(nèi)存,因此约急,這里不是重寫(xiě)alloc
方法零远,而是重寫(xiě)allocWithZone
方法。 - 提供類方法厌蔽,是為了讓外界快速創(chuàng)建單例牵辣,因此幾乎所有的單例創(chuàng)建都是這樣的寫(xiě)法了。
而對(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è)完整的單例的寫(xiě)法攒磨,其實(shí)要將copy
方法也考慮在內(nèi),應(yīng)該考慮到外界的各種創(chuàng)建方式汤徽。
接下來(lái)娩缰,咱們就說(shuō)說(shuō)在開(kāi)發(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)建單例。這樣的寫(xiě)法壳鹤,正呈⒘洌看來(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ì)開(kāi)始執(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ì)單例的正確寫(xiě)法有更深刻的理解群叶,而不僅僅是停留在copy代碼的層面。
誤區(qū)二:有些童鞋為了簡(jiǎn)化代碼钝荡,使用繼承的方式來(lái)創(chuàng)建新的單例類街立。
由于單例的創(chuàng)建代碼都是一樣的,因此埠通,有些童鞋為了避免多次重復(fù)編寫(xiě)創(chuàng)建單例的代碼赎离,就想通過(guò)繼承的方式來(lái)省去創(chuàng)建單例的代碼。于是端辱,就會(huì)在寫(xiě)好一個(gè)單例后梁剔,讓其他單例都繼承自這個(gè)類。
比如說(shuō)舞蔽,文章一開(kāi)始創(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í)際開(kāi)發(fā)中使用單例驻仅,切勿用繼承的方式來(lái)省去創(chuàng)建單例的代碼谅畅。
那么,既然創(chuàng)建單例的代碼都是一樣的噪服,如何能夠做到不重復(fù)編寫(xiě)呢毡泻?答案:將單例的創(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
文件揽惹,然后寫(xiě)上 .h
對(duì)應(yīng)的宏定義
#import <Foundation/Foundation.h>
#import "WSHLSingleton.h" // 單例宏定義對(duì)應(yīng)的頭文件
@interface WSHLCar : NSObject
WSHLSingletonH
@end
.m
文件中被饿,寫(xiě)上 .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ù)即可,這里就不再羅列代碼了囱嫩,有興趣的童鞋自行搞一下好啦恃疯。