單例的使用--誤區分析
上一篇文章中提到,最近在回顧GCD及單例相關的一些知識點,這篇文章,就重點說一下單例的使用過程中需要注意的地方。
首先,咱們分析一下單例的存在意義。
對於某個類來說,其物件以單例的形式存在,目的是為了使整個程式中只有唯一一個例項物件,整個程式中只有一份該物件的記憶體地址。外界無論有多少次的建立程式碼,拿到的都只是最初建立的那個例項物件。
然後,咱們重點來看單例的寫法。
新建一個person類:
.h檔案
#import <Foundation/Foundation.h> @interface WSHLPerson : NSObject + (instancetype)sharedInstance; @end
.m檔案
@implementation WSHLPerson static id _instance; /** 提供類方法快速建立單例物件 */ + (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 /** 實現NSCopying代理方法 */ - (nonnull id)copyWithZone:(nullable NSZone *)zone { return _instance; } @end
上面這段示例程式碼,就是建立單例的完整過程:
1.申明static 例項變數
→
2.重寫allocWithZone 方法
→
3.給外界提供快速建立單例的類方法
→
4.實現NSCopying 協議下的copyWithZone 方法。
相信大多數童鞋對於前三步應該都沒有上面問題。
一、給例項前面加上static ,是為了不被外界訪問。如果沒有static ,那麼外界完全可以通過sharedInstance 拿到單例物件,萬一外界清空了這個單例物件,那麼這個單例就永久性的失去意義了,而且還沒辦法二次建立了,因為建立單例的程式碼是一次性的(GCD--once ),因此此處必須加上static 關鍵詞。
二、重寫allocWithZone 方法的目的是為了保證單例的唯一性。因為外界可能會不用類方法來建立物件,而是通過常用的alloc 方法來建立物件。有些童鞋可能會問:為什麼不是重寫alloc 方法,而是重寫allocWithZone 方法呢?這是因為alloc方法其實最終還是會呼叫allocWithZone 方法來分配記憶體,因此,這裡不是重寫alloc方法,而是重寫allocWithZone 方法。
三、提供類方法,是為了讓外界快速建立單例,因此幾乎所有的單例建立都是這樣的寫法了。
而對於第四步,可能有些童鞋就略顯陌生了,會心存疑慮:為什麼要實現NSCopying 協議下的copyWithZone 方法呢??請看下面分析:
如果外界通過已有物件person,利用copy 方法[person copy] 來建立另一個物件時,copy 方法會再呼叫copyWithZone 方法來建立物件,而如果單例所在的類內部沒有實現copyWithZone 方法,那麼就會發生crash ,crash 的原因即為:單例內部沒有找到copyWithZone 方法。
因此,一個完整的單例的寫法,其實要將copy 方法也考慮在內,應該考慮到外界的各種建立方式。
接下來,咱們就說說在開發過程中使用單例時,幾種可能存在的誤區。
1.有些童鞋不通過GCD-once (一次性程式碼)來建立單例,而是通過if條件語句判斷例項物件是否為nil來建立。
來看程式碼:
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; }
建立單例的過程,並沒有使用GCD-once (一次性程式碼),而是通過判斷靜態例項是否已經存在來建立單例。這樣的寫法,正常看來是沒有問題的,但是,還是存在一定的隱患 →→ 多執行緒的情況。
咱們來具體分析一下:
現在有兩項任務,需要運用併發佇列 + 非同步函式開啟兩條執行緒(執行緒A、執行緒B)來執行,而執行緒A與執行緒B所要執行的任務中都需要用到person單例,此時,如果通過上述程式碼來獲取單例,就會出現隱患了。當執行緒A來到allocWithZone 方法時,發現單例是nil ,所以,就會準備 執行 _instance = [super allocWithZone:zone] ,注意,這裡說到是“準備要執行”,而正好在這個時候,執行緒B也來到了 allocWithZone 方法,判斷髮現單例還是nil ,所以,執行緒B也會開始執行 _instance = [super allocWithZone:zone] ,這樣一來,就會創建出兩個單例物件(二者記憶體地址不一樣),這就失去了單例存在的意義,進而就會引發一連串類似於資料對應不一致的問題了。。
這樣的問題是有可能發生的,而且不好重現,更不好定位問題的癥結所在,所以會很頭疼。。。。
而通過GCD-once(一次性程式碼)來建立單例物件,就會避免這樣的問題。首先,GCD-once(一次性程式碼)內部是執行緒安全的,這就已經排除了隱患,其次,GCD-once作為全域性的一次性程式碼,無論是否為多執行緒,只要有一條執行緒已經進入到了GCD-once的內部程式碼,那麼其他執行緒即便是原本需要執行該程式碼,也不會執行了。
分析這個誤區,一是希望有這種誤區的童鞋儘早認清其隱患,二是希望使用單例的童鞋們能對單例的正確寫法有更深刻的理解,而不僅僅是停留在copy程式碼的層面。
2.單例的使用,切不可用繼承(某個單例繼承自某個單例)。
因為單例的建立程式碼都是一樣的,所以,有些童鞋為了避免多次重複編寫建立單例的程式碼,就想通過繼承的方式來省去建立單例的程式碼。於是,就會在寫好一個單例後,讓其他單例都繼承自這個類。
比如說,文章一開始建立單例的例項程式碼中,建立了一個WSHLPerson類的單例,此時,又有WSHLChinese和WSHLAmerican兩個類也是需要運用單例模式,於是,有些童鞋就會直接讓這兩個類繼承自WSHLPerson,然後在這兩個類中什麼都不做。因為反正父類中提供了建立單例的 sharedInstance 方法。
下面,咱們就通過實際程式碼測試,來看一下這樣做到底可不可以。
在控制器中引入建立的WSHLChinese和WSHLAmerican這兩個類的.h檔案,然後在控制器的 touchesBegan 方法中,分別建立WSHLChinese和WSHLAmerican單例物件
然後咱們列印一下這兩個單例物件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"----%@----",[WSHLChinese sharedInstance]); NSLog(@"----%@----",[WSHLAmerican sharedInstance]); }
得到的結果如下:
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>----
輸入結果顯示,建立的兩個單例都是屬於WSHLChinese類。為什麼會這樣呢?
因為建立單例的程式碼是通過GCD-once(一次性程式碼)完成的,整個程式只會執行一次這段程式碼,因此,由於是建立WSHLChinese單例的程式碼在前,所以當WSHLChinese這個單例建立完成後,單例(全域性的static)已經存在了,那麼後續的WSHLAmerican建立單例時,就不會再執行GCD-once程式碼了,而是直接返回建立好的單例物件了,也就是WSHLChinese單例。同樣的,如果將上面兩行程式碼互換位置,先建立WSHLAmerican單例,後建立WSHLChinese單例,那麼結果就會是兩個單例都是WSHLAmerican類。
因此,在實際開發中使用單例,切勿用繼承的方式來省去建立單例的程式碼。
那麼,既然建立單例的程式碼都是一樣的,如何能夠做到不重複編寫呢?答案:將單例的建立過程封裝在一個巨集定義裡面 。
新建一個繼承自NSObject的類,將.m檔案delete,.h檔案中的所有預備程式碼全部delete。然後定義兩個巨集,分別對應單例所在類的.h和.m檔案中的程式碼:
/** .h檔案 */ #define WSHLSingletonH + (instancetype)sharedInstance; /** 。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檔案中加入兩個巨集就可以了。下面以Car為例,示範一下:
.h檔案
#import <Foundation/Foundation.h> #import "WSHLSingleton.h" @interface WSHLCar : NSObject WSHLSingletonH @end
.m檔案
#import "WSHLCar.h" @implementation WSHLCar WSHLSingletonM @end
使用這個單例:
NSLog(@"%@",[WSHLCar sharedInstance]);
這樣,就解決了重複程式碼的問題了。
當然了,可能有些童鞋建立單例的方法名不喜歡用通用的 sharedInstance,而是想用 sharedCar、sharedPerson等等,這也很簡單,只要在巨集定義裡面加上個引數即可,這裡就不再羅列程式碼了,有興趣的童鞋自行搞一下好啦~~
好了,以上就是關於單例的使用自己的一些見解與看法,希望園友們多多指正不正確的地方,共同學習,共同進步。。