設計模式之單例模式
認真對待每時、每刻每一件事,把握當下、立即去做。
單例模式,由于其簡單好用容易理解、同時在出問題時也容易定位的特點,在開發中經常用到的一個設計模式。
一.什么是單例模式
1. 單例模式的定義
簡單的來說,一個單例類,在整個程序中只有一個實例,并且提供一個類方法供全局調用,在編譯時初始化這個類,然后一直保存在內存中,到程序(APP)退出時系統自動釋放這部分內存。
2. 系統單例類了解
UIApplication(應用程序實例類)
NSNotificationCenter(消息中心類)
NSFileManager(文件管理類)
NSUserDefaults(應用程序設置)
NSURLCache(請求緩存類)
NSHTTPCookiesStorage(應用程序cookies池)
3. 在那些地方會常用到單例類
一般在應用程序中,經常需要調用的類,比如工具類,公共跳轉類等等,都建議使用單例模式。
二.單例模式的生命周期
1. 單例實例在存儲器中的位置
程序中不同變量在手機存儲器中的存儲位置詳情見“內存管理”專題。
在程序中,一個單例類在只能初始化一次,為了保證在使用時始終都是存在的,所以單例是在存儲器的全局區域。在編譯時分配內存,只要程序還在運行就會一直占用內存,在 APP 結束后由系統釋放這部分內存。
2. 多次初始化單例類會發生什么?
下面代碼我們在工程中初始化一次 UIApplication。最終運行的結果如下,程序直接崩潰,由此可以確定,一個單例類只能初始化一次。
[[UIApplication alloc] init];
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'There can only be one UIApplication instance.'
三.實現單例類
1. 實現單例類的思路
只能分配一次內存(初始化一次),因此要攔截 alloc 方法。alloc 方法的底層是 allocWithZone 方法。
每個類只有一個對象,需要一個全局靜態變量來存儲這個對象。
需要考慮線程安全。
2. 單例的兩種模式
懶漢模式:當使用這個單例對象的時候,才創建對象,就是 _instance 的懶加載形式。由于移動設備內存有限,所以這種方式最適合。
餓漢模式:當類第一次加載的時候,就創建單例對象,并保存在 _instance 中。由于第一次加載就創建,內存從程序開始運行的時候就分配了,不適合移動設備。
3. 單例基本形式(懶漢模式)
@interface XBLoadTool : NSObject
// 給外界快速創建單例對象使用
+ (instancetype)sharedLoadTool;
@end
#import "XBLoadTool.h"
// 定義全局靜態變量,用來存儲創建好的單例對象,當外界需要時,返回
static id _instance;
@implementation XBLoadTool
// 給外界快速創建單例對象使用
+ (instancetype)sharedLoadTool {
if (_instance == nil) {
// 避免出現多個線程同時創建_instance,加鎖
@synchronized (self) {
// 使用懶加載,確保_instance只創建一次
if (_instance == nil) {
_instance = [[self alloc] init];
}
}
}
return _instance;
}
// 重寫allocWithZone:方法---內存與sharedLoadTool方法體基本相同
+ (instancetype)allocWithZone:(NSZone *)zone {
// 避免每次線程過來都加鎖,首先判斷一次,如果為空才會繼續加鎖并創建對象
if(_instance == nil) {
// 避免出現多個線程同時創建_instance,加鎖
@synchronized(self) {
// 使用懶加載,確保_instance只創建一次
if(_instance == nil) {
//調用父類方法,分配空間
_instance = [super allocWithZone:zone];
}
}
}
return _instance;
}
// 重寫copyWithZone:方法,避免實例對象的copy操作導致創建新的對象
- (instancetype)copyWithZone:(NSZone *)zone {
// 由于是對象方法,說明可能存在_instance對象,直接返回即可
return _instance;
}
@end
為什么全局變量要使用 static 修飾?
static 修飾局部變量:
- 其生命周期與全局變量相同,直到程序結束,只有一份內存空間。
- 內存空間:僅有一份,多次調用函數時保留上次的值。
- 作用域不變,僅限定義它的函數或代碼塊內,與未加static的局部變量作用域一致。
static 修飾全局變量:
- 內存空間:只有一份內存空間,全局變量本身只有一份,static 修飾后不改變此特性。
- 全局變量可以在其他文件中,通過 extern id _instance 來聲明,然后直接在其他文件中調用。用 static 會將全局變量的鏈接屬性從 external 改為 internal,使其僅在當前文件可見,其他文件無法通過
extern引用,任何方式都無法跨文件訪問。
加鎖且懶加載的原理:懶加載是為了,確保整個類只有一個 instance。加鎖:多線程中,可能多個線程都發現當前的 _instance==nil,那么就會同時創建對象,不符合單例的原則,所以加鎖。但是加鎖容易引起效率降低,不能每次線程過來就加鎖,所以在加鎖之前首先判斷一次是否為空,不為空根本不需要創建,直接返回。為空則說明可能需要創建對象,那么再加鎖。
3. GCD(dispatch_once_t)創建單例(懶漢模式)
考慮到線程安全,蘋果官方推薦開發者使用 dispatch_once_t 來創建單例類。上面實例中,在 allocWithZone 方法和 sharedLoadTool 中,每次需要判斷是否為空,然后加鎖,其目的是為了保證 [[self alloc]init] 和 [super allocWithZone:zone] 代碼只執行一次,那么可以使用 GCD 的一次性代碼解決,另外,GCD 一次性代碼是線程安全的,所以不需要我們自己來處理加鎖問題。
// 創建單例類方法,供全局調用 - retutn type instancetype
+ (instancetype)shareOnceClass {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_onceClass = [[XBOnceClass alloc] init];
});
return _onceClass;
}
// 修改 allocWithZone 方法
+ (instancetype)allocWithZone:(NSZone *)zone{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_onceClass = [super allocWithZone:zone
];
});
return _onceClass;
}
// 重寫 copyWithZone:方法,避免實例對象的 copy 操作導致創建新的對象
-(instancetype)copyWithZone:(NSZone *)zone
{
//由于是對象方法,說明可能存在_onceClass對象,直接返回即可
return _onceClass;
}
由于移動端特性,我們在開發過程中多用 GDG(懶漢模式)來創建單例。對于餓漢模式在第五大點有提到。
四.單例模式的優缺點
優點:
1)在整個程序中只會實例化一次,所以在程序如果出了問題,可以快速的定位問題所在;
2)由于在整個程序中只存在一個對象,節省了系統內存資源,提高了程序的運行效率;
缺點:
1)不能被繼承,不能有子類;
2)不易被重寫或擴展(可以使用分類);
3)同時,由于單例對象只要程序在運行中就會一直占用系統內存,該對象在閑置時并不能銷毀,在閑置時也消耗了系統內存資源;
五.單例模式過程詳解
1. 初始化過程解析
重寫單例類的 alloc->allocWithZone 方法,確保這個單例類只被初始化一次。
在 viewDidLoad 方法中調用單例類的 alloc 和 init 方法:[[XBOnceClass alloc] init];
此時只是報黃點,但是并沒有報錯,Run 程序也可以成功,這樣的話,就不符合我們最開始使用單例模式的初衷來,這個類也可以隨便初始化類,為什么呢?因為我們并沒有獲取 OneTimeClass 類中的使用實例;
因此可以重寫 alloc 方法的處理可以采用斷言或者系統為開發者提供的 NSException 類來告訴其他的同事這個類是單例類,不能多次初始化。
// 斷言
+ (instancetype)alloc {
NSCAssert(!_onceClass, @"單例XBOnceClass只能被初始化一次");
return [super alloc];
}
//NSException
+ (instancetype)alloc {
//如果已經初始化了
if (_onceClass) {
NSException *exception = [NSException exceptionWithName:@"提示" reason:@"XBOnceClass類只能初始化一次" userInfo:nil];
[exception raise];
}
return [super alloc];
}
但是,如果我們的程序直接就崩潰了,這樣的做法與開發者開發 APP 的初衷是不是又相悖了,作為一個程序員的目的要給用戶一個交互友好的 APP,而不是一點小問題就崩潰。對于這種情況,可以用到 NSObect 類提供的 load 方法和 initialize 方法來控制,
這兩個方法的調用時機,load 方法:當程序開始運行的時候,所有類都會加載到內存中(不管這個類有沒有使用),此時就會調用 load 方法,如果想某個操作在程序運行的過程中只執行一次,那么這個操作就可以放到 load 中,且在 main 函數調用之前調用,基于以上特點餓漢模式的單例創建就是放在 load 方法中; initialize 方法是當類第一次被使用的時候調用(比如調用類的方法),在 main 函數調用之后調用,如果子類沒有重寫該方法,那么父類的 initialize 方法可能會被執行多次,所以餓漢模式不能使用這種方法;
這樣的話,餓漢模式下,如果我在單例類的 load 方法初始化這個類,是不是就保證了這個類在整個程序中調用一次呢?
這樣就可以保證 sharedMusicTool 方法是最早調用的。同時,再次對 alloc 方法修改,無論在何時調用 instance 已經初始化了,如果再次調用 alloc 可直接返回_instance 實例。
@interface XBMusicTool : NSObject
//提供外界訪問的方法
+(instancetype)sharedMusicTool;
@end
#import "XBMusicTool.h"
// 定義靜態全局變量
static id _instance;
@implementation XBMusicTool
// 實現方法
+ (instancetype)sharedMusicTool {
return _instance;
}
// 重寫load方法
+ (void)load {
// 不需要線程安全,類加載的時候線程還沒開始呢
_instance = [[self alloc]init];
}
// 重寫allocWithZone方法
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
if(_instance == nil) {
_instance = [super allocWithZone:zone];
}
return _instance;
}
// 重寫copyWithZone:方法,避免實例對象的copy操作導致創建新的對象
-(instancetype)copyWithZone:(NSZone *)zone {
// 由于是對象方法,說明可能存在_instance對象,直接返回即可
return _instance;
}
@end
最后在 ViewController 中打印調用 XBMusicTool 的 sharedMusicTool 和 alloc 方法,可以看到 Log 出來的內存地址是相同的,這就說明此時我的 XBMusicTool 類就只初始化了一次。
2. 直接禁止方法的使用
直接禁用方法,禁止調用這幾個方法,否則就報錯,編譯不過,不建議使用。
-(instancetype) copy __attribute__((unavailable("OneTimeClass類只能初始化一次")));
六.常見問題和學習
1. 如果單例的靜態變量被置為 nil 了,是否內存會得到釋放?
https://blog.csdn.net/jhcBoKe/article/details/108097693

浙公網安備 33010602011771號