iOS中的單例模式

@WilliamAlex大叔

前言

目前流行的社交APP中都離不開單例的使用,我們來舉個例子哈,比如現在流行的"糗事百科""美拍"等APP中,當你選擇某一個功能時,它都會跳轉到登錄界面,然而登錄界面都是一樣的,所以我們完全可以將這個登錄控制器設置成一個單例.這樣可以節省內存的開銷,優化我們的內存,下面純屬個人整理,如果有錯誤,希望大家指出來,相互進步.下面我們正式開始介紹單例

單例模式

  • 單例模式的作用
  • 確保在程序運行的過程中,一個類或者是一個對象只有一個實例,一個內存,并且該實例易被外界訪問.
  • 單例模式的使用場合
  • 在整個應用程序中,共享一份資源,這份資源只需要創建初始化1次,就比如前言中所描述的登錄界面.
  • 單例模式的實例
  • 獲取主窗口 : [UIApplication sharedApplication]
  • 獲取某個目錄下的文件資源 : [NSFileManager defaultManager]
  • 數據存儲中的偏好設置 : [NSUserDefaults standardUserDefaults]

在寫代碼之前,我們好好整理整理思路

  • 學習單例的最好方法是從內存地址入手,因為單例的本質是只會創建一份實例,說明它只有一份內存,我們可以通過內存地址觸發,慢慢了解單例的好處以及優勢.
  • 本章主要介紹兩種方式創建單例(使用GCD方式和普通創建單例方式)
  • GCD方式 : dispatch_once_t
  • 普通方式 : if/else語句, @synchronized(加鎖)聯用

引入單例

  • 我們通過新建一個WGStudent類,在ViewController中創建多個WGStudnt類型的對象,打印出它們的地址
// 不要忘記需要導入頭文件哦

- (void)viewDidLoad {
    [super viewDidLoad];
    // 創建對個對象
    WGStudent *student1 = [[WGStudent alloc] init];
    WGStudent *student2 = [[WGStudent alloc] init];
    WGStudent *student3 = [[WGStudent alloc] init];
    WGStudent *student4 = [[WGStudent alloc] init];
    WGStudent *student5 = [[WGStudent alloc] init];

    // 打印對應的地址
    NSLog(@"S1=%p,S2=%p,S3=%p,S4=%p,S5=%p",student1,student2,student3,student4,student5);
}

打印結果

S1=0x7ff4fae07a30
S2=0x7ff4fae0e520
S3=0x7ff4fae04580
S4=0x7ff4fae0e390
S5=0x7ff4fae0e430

  • 總結 : 通過上述示例,每次都會alloc一次,導致它們的內存地址不一樣,但是我們最初的目的只是創建同一個對象,我們都知道,只要alloc一次,系統就會開辟一個新的存儲空間,但是根據我們的要求,完全是沒有必要另辟新的存儲空間的.所以這時候我們就需要引入單例模式.

單例模式的原理

  • 原理 : 根據上面的示例,我們可以很清楚的明白,既然我們想要它多次創建,但是只有一份內存,我們只需要重寫alloc方法即可吖,在重寫的方法中確保進來的對象只創建一次.不錯,思路是正確的,但是我們要弄清楚本質,什么才是最嚴謹的做法.
  • 其實我們這里并不是重寫alloc方法,創建對象,調用alloc,其實它的本質是調用了alloc的底層:allocWithZone方法,所以我們實現單例模式重寫的是allocWithZone而不是alloc方法.
  • 我們只需要保證整個進程中,allocWithZone只會調用1次即可實現單例模式.
  • 現在我們的目標是將上面的打印中的地址變成同一個內存地址.

創建單例的格式

  • 給外界提供一個接口 :

  • 說明自己的身份,讓別人一看就知道它是一個單例

  • 命名規范:share+類名|default+類名|share|類名|standard + 類名

  • 既然做了,我們就要做到最嚴謹,不管是外界 alloc、init 還是 copy,mutableCopy 都應當只有一份實例重寫allocWithZone,讓這個方法生成實例的代碼只能運行一次即可。

GCD方式 : dispatch_once_t
步驟 :

  • 創建一個WGStudent類
  • 在.h文件中聲明一個類方法shareInstance,供給外界使用
  • 在.m文件中,重寫allocWithZone方法,保證它在整個進程中只會執行一次.
  • 實現聲明的類方法,保證它只會被初始化一次
  • 為了嚴謹起見,我們重寫copyWithZone以及MutableCopyWithZone方法,這里需要注意,重寫這兩個對象方法時,需要遵循<NSCopying,NSMutableCopying>兩個協議,這樣才能找到方法,當然,當我們重寫這兩個方法以后我們可以不遵守這兩個協議,去掉也可以(中重寫完畢兩個對象方法以后).

dispatch_once_t實現單例代碼

在WGStudent.h文件中
#import <Foundation/Foundation.h>

@interface WGStudent : NSObject

/**
 *  聲明一個類方法,表明自己是一個單例
 */
+ (instancetype)shareInstance;

@end

  • 注意:聲明單例的命名規范

  • 注意示例中提出來的問題,下面有詳細的解釋,先看明白代碼.

在WGStudent.m文件中
#import "WGStudent.h"

// 協議可以不遵守嗎? (我沒有刪掉是因為便于理解代碼)
@interface WGStudent() <NSCopying, NSMutableCopying>

@end

// onceToken的主要作用是什么?
@implementation WGStudent

// 為什么要定義一個static全局變量?
static WGStudent *_instance;

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    // 這里使用dispatch_once_t的目的是什么?
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        _instance = [super allocWithZone:zone];

    });

    return _instance;
}

+ (instancetype)shareInstance
{
    // 這里使用dispatch_once_t的目的是什么?
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        _instance = [[self alloc] init];
    });

    return _instance;
}

// 重寫下面兩個對象方法的注意點是什么
- (id)copyWithZone:(NSZone *)zone
{
    return _instance;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}

@end

打印結果

S1=0x7faceb53b2c0
S2=0x7faceb53b2c0
S3=0x7faceb53b2c0
S4=0x7faceb53b2c0
S5=0x7faceb53b2c0
  • 解釋示例中提出來的問題 :
  • 協議可以不遵守嗎? 答案是可以的,我們遵守協議的目的主要是重寫copyWithZone和mutableCopyWithZone方法(不然打不出方法來),當我們重寫完畢之后,就可以不遵守了.
  • onceToken的作用是什么? onceToken的主要作用是用來記錄當前的block是否已經執行過了,如果執行過了,那么就不要再次執行.
  • 為什么要定義一個static修飾的全局變量? 使用static修飾全局變量主要是保證只有該文件可以使用,外界是沒有辦法使用的,防止外界將指針清空(注意: static WGStudent *_instance;是一個被強指針指向的全局變量,既然是單例,就要保證在整個進程中單例對象不要釋放,也就是說,單例之所以一直存在,是因為有一個強指針指著),如果指針被清空,下面返回的值就會為nil,沒有值,還談什么單例.
  • 在allocWithZone方法中使用dispatch_once主要是保證,對象只會被創建一次,只分配一次內存.
  • 在shareInstance方法中使用dispatch_once,主要是保證只會初始化一次,比如說:初始化成員屬性.為了嚴謹起見,在類方法中不能直接返回,因為它可能第一次創建,為空返回值就會返回nil.
  • 重寫兩個對象方法的注意點是什么?前面我們已經說過了,也是為了嚴謹起見,如果外界使用copy或者mutableCopy創建對象,那我們也將它弄成單例.但是如果你直接敲copy是沒有這兩個對象方法的,我們必須要遵守<NSCopying,NSMutableCopying>兩個協議才能敲出方法,當我們重寫完畢時,你可以將協議刪掉.

普通方式if來創建單例

首先我們來寫一份不夠嚴謹的代碼,看看問題出來哪里


#import "WGStudent.h"

@interface WGStudent() <NSCopying, NSMutableCopying>

@end

@implementation WGStudent

// 定義全局變量,保證整個進程運行過程中都不會釋放
static WGStudent *_instance;

// 保證整個進程運行過程中,只會分配一個內存空間

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (nil == _instance) {
        _instance = [super allocWithZone:zone];
    }
    return _instance;
}

+ (instancetype)shareInstance
{
    if (nil == _instance) {
        _instance = [[self alloc] init];
    }
    return _instance;
}

- (id)copyWithZone:(NSZone *)zone
{
    return _instance;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}

@end

打印結果

S1=0x7febf153ba80
S2=0x7febf153ba80
S3=0x7febf153ba80
S4=0x7febf153ba80
S5=0x7febf153ba80
  • 注意 : 看到上面的打印結果,咦o,內存地址是一樣的,可以了呀,為什么還說不夠嚴謹呢.你丫裝逼失敗了吧!!!
  • 細心的朋友已經看出來是怎么回事了,用if是不夠安全的,我們忽略了多線程這點.
  • 我們來分析一下哈,假如現在有多條線程,假設線程1進入allocWithZone方法中了,判斷了一下,咦! 沒有值,線程1進來了,有可能線程1還沒有賦值,沒有分配存儲空間,線程2也進入allocWithZone方法了,判斷一下,好家伙! 也沒有值,這時候線程1已經賦值完畢,分配好了內存空間,線程2也開始了賦值,分配新的內存空間,這就造成了多次分配內存空間,這和單例模式的本質原理是相違背的.
  • 解決辦法也很簡單,給線程加鎖.

解決后的代碼

#import "WGStudent.h"

@interface WGStudent()

@end

@implementation WGStudent

// 定義全局變量,保證整個進程運行過程中都不會釋放
static WGStudent *_instance;

// 保證整個進程運行過程中,只會分配一個內存空間

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    @synchronized(self) {
        if (nil == _instance) {
            _instance = [super allocWithZone:zone];
        }
        return _instance;
    }
}

+ (instancetype)shareInstance
{
    @synchronized(self) {

        if (nil == _instance) {
            _instance = [[self alloc] init];
        }
        return _instance;
    }

}

- (id)copyWithZone:(NSZone *)zone
{
    return _instance;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}
@end

打印結果

S1=0x7fd39af539d0
S2=0x7fd39af539d0
S3=0x7fd39af539d0
S4=0x7fd39af539d0
S5=0x7fd39af539d0
  • 注意 : 使用線程加鎖一定要注意它的位置. 線程加鎖的鎖對象一般是當前類(self)原因是當前類也是只有一個內存,唯一的.

以上就是實現在ARC環境下創建單例的兩種方法

接下來我們來創建MRC環境下的單例

設置環境.png
  • 我們來分析一下哈,ARC與MRC的主要區別是什么(具體的區別后續我會更新的),主要區別就是是否需要手動管理內存.

下面是MRC環境下的代碼

在.h文件中聲明單例方法
#import <Foundation/Foundation.h>

@interface WGStudent : NSObject

/**
 *  聲明一個類方法,表明自己是一個單例
 */
+ (instancetype)shareInstance;

@end
在.m文件中重寫方法

#import "WGStudent.h"

@interface WGStudent()

@end

@implementation WGStudent

#pragma mark - ARC環境下的單例
// 定義全局變量,保證整個進程運行過程中都不會釋放
static WGStudent *_instance;

// 保證整個進程運行過程中,只會分配一個內存空間

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    @synchronized(self) {
        if (nil == _instance) {
            _instance = [super allocWithZone:zone];
        }
        return _instance;
    }
}

+ (instancetype)shareInstance
{
    @synchronized(self) {

        if (nil == _instance) {
            _instance = [[self alloc] init];
        }
        return _instance;
    }

}

- (id)copyWithZone:(NSZone *)zone
{
    return _instance;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}

#pragma mark - MRC環境下的單例(還要加上上面的方法)

#if __has_feature(objc_arc)
// ARC :就執行上面重寫的方法即可
#else
// MRC : 除了執行上面的方法,還需要重寫下面的方法.

- (oneway void)release {

    // 什么都不用做,安靜的看著其他方法裝逼即可
}

- (instancetype)retain
{
    return _instance;
}

- (NSUInteger)retainCount
{
    return MAXFLOAT;
}
#endif

@end

打印結果

S1=0x7f9b6bd90fb0
S2=0x7f9b6bd90fb0
S3=0x7f9b6bd90fb0
S4=0x7f9b6bd90fb0
S5=0x7f9b6bd90fb0
  • 注意點 :
  • 需要將ARC環境設置為MRC環境
  • 示例中我講ARC和MRC都混合在了一起,需要記住判斷當前環境是ARC還是MRC的宏
- (void)currentEnvironment
{
#if __has_feature(objc_arc)
        //  ARC
        NSLog(@"ARC環境");
#else
        //  MRC
        NSLog(@"MRC環境");
#endif
}

以上就是ARC和MRC環境下的單例

  • 在實際開發中,我們為了提高工作效率,一般不會每次需要使用單例時,都老實巴交一步一步的編寫單例,我習慣將他們抽取出來,定義成一個宏,到時候使用單例時,直接調用宏,我們只需要傳入一個參數.

單例宏代碼

// 直接將單例的實現(ARC和MRC)全部定義到PCH文件中,,設置PCH文件路徑即可
#define SingleH(instance) +(instancetype)share##instance;

#if __has_feature(objc_arc)
//ARC
#define SingleM(instance) static id _instance;\
\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
+(instancetype)share##instance\
{\
return [[self alloc]init];\
}\
\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}
#else

//MRC
#define SingleM(instance) static id _instance;\
\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
+(instancetype)share##instance\
{\
return [[self alloc]init];\
}\
\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(oneway void)release\
{\
}\
-(instancetype)retain\
{\
    return _instance;\
}\
\
-(NSUInteger)retainCount\
{\
    return MAXFLOAT;\
}
#endif

  • 注意點 :
  • 每一行都需要''不然下一行不能識別
  • 不要在注釋后面添加''.否則后面的全部都會變成注釋
  • 在實際開發中,我們可以定義的方法不一樣,我們可以使用"##"兩個井號讓方法變成可變的參數,我們傳入什么,它就是什么.
  • 注意定義全局變量的時候,我們定義的類是不一樣的,所以我們需要將它改為id類型.

這里需要重點聽 : 有的初學者朋友可能會使用繼承,這樣就不用把它定義成宏了,我上面就說過了,我們千萬不能在單例中使用繼承,原因我們看代碼,不要耍流氓

使用繼承

  • 使用繼承,首先創建一個父類,WGSignaltonTool,在父類的.h文件中聲明單例方法,在.m文件中實現單例方法
在.h文件中
#import <Foundation/Foundation.h>

@interface WGSignaltonTool : NSObject

/**
 *  聲明一個類方法,表明自己是一個單例
 */
+ (instancetype)shareInstance;

@end


在.m文件中
#import "WGSignaltonTool.h"

@implementation WGSignaltonTool

#pragma mark - ARC環境下的單例
// 定義全局變量,保證整個進程運行過程中都不會釋放
static WGSignaltonTool *_instance;

// 保證整個進程運行過程中,只會分配一個內存空間

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    @synchronized(self) {
        if (nil == _instance) {
            _instance = [super allocWithZone:zone];
        }
        return _instance;
    }
}

+ (instancetype)shareInstance
{
    @synchronized(self) {

        if (nil == _instance) {
            _instance = [[self alloc] init];
        }
        return _instance;
    }

}

- (id)copyWithZone:(NSZone *)zone
{
    return _instance;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}

#pragma mark - MRC環境下的單例(還要加上上面的方法)

#if __has_feature(objc_arc)
// ARC :就執行上面重寫的方法即可
#else
// MRC : 除了執行上面的方法,還需要重寫下面的方法.

- (oneway void)release {

    // 什么都不用做,安靜的看著其他方法裝逼即可
}

- (instancetype)retain
{
    return _instance;
}

- (NSUInteger)retainCount
{
    return MAXFLOAT;
}
#endif

@end

創建兩個子類:WGPerson和WGStudent,分別繼承WGSignaltonTool,兩個子類只需要繼承父類即可,什么都不用寫

  • 繼承完畢父類,我們來到ViewController.m文件,導入兩個子類,然后在ViewDidLoad中打印它們的內存地址.

  • 只打印WGPerson類的地址(單獨打印WGStudent類的地址情況和WGPerson類類似,所以,這里就打印一個啦)

NSLog(@"%@,%@",[WGPerson shareInstance],[[WGPerson alloc] init]);

打印結果

<WGPerson: 0x7f9912d93b40>
<WGPerson: 0x7f9912d93b40>
  • 結論 : 感覺使用繼承也是可以的吖,打印出來的地址是一樣的,我們先別著急,我們接著來看兩個一起打印是是什么結果.
NSLog(@"%@,%@",[WGStudent shareInstance],[[WGStudent alloc] init]);
NSLog(@"%@,%@",[WGPerson shareInstance],[[WGPerson alloc] init]);

打印結果

單例[1569:88929] <WGStudent: 0x7f9daa4032f0>,<WGStudent: 0x7f9daa4032f0>
單例[1569:88929] <WGStudent: 0x7f9daa4032f0>,<WGStudent: 0x7f9daa4032f0>

  • 總結 : 你看哈,當我們兩個子類一起打印的時候們就會發現,打印出來的結果全是WGStudent,地址也是一樣的.這明顯是不對的,所以我們千萬不能使用繼承.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容

  • 單例模式大概是設計模式中最簡單的一個。本來沒什么好說的,但是實踐過程中還是有一些坑。所以本文小結一下在iOS開發中...
    qiushuitian閱讀 3,423評論 1 21
  • 單例模式是日常開發工作中經常會用到的一種設計模式。通過單例模式,可以保證程序中的一個類只有一個實例,從而方便對實例...
    狼鳳皇閱讀 191評論 0 0
  • 單例模式的作用:保證在程序運行過程中,一個類只有一個實例對象,節約系統資源。 單例模式使用場合:在整個應用程序中,...
    Xcode10閱讀 366評論 0 0
  • iOS開發中常用到2中設計模式,分別是代理模式和單例模式,本文主要介紹下單例模式 單例模式的作用 可以保證在程序運...
    Andyzhao閱讀 974評論 0 10
  • 晚上回到家里,做的第一件事就是把自己扔到沙發里,懶得動,指使孩子的爸去燒洗腳水。然后打開微信,等著泡腳。 當洗腳水...
    我是慕一閱讀 372評論 2 3