@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環境下的單例
- 我們來分析一下哈,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,地址也是一樣的.這明顯是不對的,所以我們千萬不能使用繼承.