單例的使用--誤區(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)看單例的寫法。
以新建一個(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)題。

  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. 重寫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方法。
  3. 提供類方法,是為了讓外界快速創(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í),又有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í)際開發(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,而是想用sharedCarsharedPerson等等,這也很簡(jiǎn)單,只要在宏定義里面加上個(gè)參數(shù)即可,這里就不再羅列代碼了,有興趣的童鞋自行搞一下好啦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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