單例的使用--誤區(qū)分析

時(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)題撵儿。

  1. 給實(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)鍵詞。
  2. 重寫(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方法。
  3. 提供類方法厌蔽,是為了讓外界快速創(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í)渗柿,又有WSHLChineseWSHLAmerican兩個(gè)類也是需要運(yùn)用單例模式个盆,于是,有些童鞋就會(huì)直接讓這兩個(gè)類繼承自WSHLPerson,然后在這兩個(gè)類中什么都不做颊亮。因?yàn)榉凑割愔刑峁┝藙?chuàng)建單例的 sharedInstance 方法柴梆。

下面,咱們就通過(guò)實(shí)際代碼測(cè)試终惑,來(lái)看一下這樣做到底可不可以绍在。

  1. 創(chuàng)建WSHLChineseWSHLAmerican,讓這兩個(gè)類都繼承自WSHLPerson狠鸳。
  2. 在控制器中引入創(chuàng)建的WSHLChineseWSHLAmerican這兩個(gè)類的.h文件揣苏,然后在控制器的 touchesBegan 方法中,分別創(chuàng)建WSHLChineseWSHLAmerican單例對(duì)象件舵。
  3. 打印一下這兩個(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狭握,而是想用sharedCarsharedPerson等等疯溺,這也很簡(jiǎn)單论颅,只要在宏定義里面加上個(gè)參數(shù)即可,這里就不再羅列代碼了囱嫩,有興趣的童鞋自行搞一下好啦恃疯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市墨闲,隨后出現(xiàn)的幾起案子今妄,更是在濱河造成了極大的恐慌,老刑警劉巖鸳碧,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盾鳞,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡杆兵,警方通過(guò)查閱死者的電腦和手機(jī)雁仲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)仔夺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)琐脏,“玉大人,你說(shuō)我怎么就攤上這事缸兔∪杖梗” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵惰蜜,是天一觀的道長(zhǎng)昂拂。 經(jīng)常有香客問(wèn)我,道長(zhǎng)抛猖,這世上最難降的妖魔是什么格侯? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任鼻听,我火速辦了婚禮,結(jié)果婚禮上联四,老公的妹妹穿的比我還像新娘撑碴。我一直安慰自己,他們只是感情好朝墩,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布醉拓。 她就那樣靜靜地躺著,像睡著了一般收苏。 火紅的嫁衣襯著肌膚如雪亿卤。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天鹿霸,我揣著相機(jī)與錄音排吴,去河邊找鬼。 笑死懦鼠,一個(gè)胖子當(dāng)著我的面吹牛傍念,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播葛闷,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼憋槐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了淑趾?” 一聲冷哼從身側(cè)響起阳仔,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扣泊,沒(méi)想到半個(gè)月后近范,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡延蟹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年评矩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阱飘。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斥杜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沥匈,到底是詐尸還是另有隱情蔗喂,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布高帖,位于F島的核電站缰儿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏散址。R本人自食惡果不足惜乖阵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一宣赔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞪浸,春花似錦拉背、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至齐蔽,卻和暖如春两疚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背含滴。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工诱渤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谈况。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓勺美,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親碑韵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赡茸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容