文章目錄
- 前言
- 一、什么是單例模式
- 二、單例模式的優缺點
- 優點
- 缺點
- 三、模式介紹
- 1.懶漢模式(GCD & 互斥鎖)
- GCD 寫法
- 互斥鎖寫法(雙重檢查鎖)
- 2.餓漢模式
- 總結
- 懶漢式 + 互斥鎖(Mutex)
- **懶漢式 + GCD (dispatch_once) **
- 餓漢式
前言
這篇博客是對單例模式的一個深入了解,之前的對單例模式的學習太過淺薄了,所以撰寫這篇博客來去深入對單例的理解。
一、什么是單例模式
單例模式 是一種常見的設計模式,核心思想是: 保證一個類在整個程序運行期間,只有唯一一個實例,并且提供一個全局訪問點。它可以做到大大減少內存的使用,防止一個實例被重復創建從而占用內存空間,他在共享資源和對象的情況下非常有用。
下面給出一些 OC 中常見的單例
- NSUserDefaults → 用戶偏好設置
- UIApplication → App 的入口對象
- NSFileManager → 文件管理器
- NSNotificationCenter → 通知中心
二、單例模式的優缺點
優點
- 使整個應用內只有一個實例,避免數據沖突。
- 提供了一個全局訪問的點,使用方便,不需要每次都 alloc/init。
- 減少了內存開銷,節省資源。
- 數據的一致性。
缺點
- 因為是全局的狀態,增加了耦合,相當于一個全局變量,如果被濫用,會導致模塊之間的依賴性增強,代碼的維護度降低。
- 單例對象的生命周期和應用的周期一樣長,很難被替換。
- 在測試的時候很難被替換掉。
- 使用單例的類,不顯式傳入依賴,而是全局取對象,導致代碼邏輯不透明。
- 而且他因為生命周期太長導致一直持有大量資源,內存占用過高。
三、模式介紹
1.懶漢模式(GCD & 互斥鎖)
在這里我給出兩個懶漢模式
要知道在寫任何單例模式的時候都需要重寫三個方法因為我們要讓alloc和copy還有mutablecopy這三個方法返回的單例保持一致,所以我們要重寫如下幾個方法。
+ (instancetype)allocWithZone:(struct _NSZone *)zone
- (id)copyWithZone:(NSZone *)zone
-(id)mutableCopyWithZone:(NSZone *)zone
GCD 寫法
這個寫法是蘋果公司最推薦的寫法,下面我會給出代碼以及解釋為什么要推薦這種寫法。
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface LazyGcdSingleton : NSObject<NSCopying, NSMutableCopying>
+ (instancetype) sharedInstance;
@endNS_ASSUME_NONNULL_END
//懶漢式GCD寫法
#import "LazyGcdSingleton.h"
static id _instance = nil;
@implementation LazyGcdSingleton
+ (instancetype)sharedInstance {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{_instance = [[super allocWithZone:NULL] init];});return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {return [LazyGcdSingleton sharedInstance];
}
- (id)copyWithZone:(NSZone *)zone {return [LazyGcdSingleton sharedInstance];
}
- (id) mutableCopyWithZone:(NSZone *)zone {return [LazyGcdSingleton sharedInstance];
}
@end
- static dispatch_once_t onceToken;
- 定義了一個靜態變量 onceToken,它的作用就是 保證下面的 block 只會執行一次。
- dispatch_once_t 是蘋果專門設計的類型,用來做單例的線程安全初始化。
- dispatch_once(&onceToken, ^{ … });
- dispatch_once 內部幫你做了 加鎖 + 判斷是否執行過 的工作。
- 無論多少線程同時調用 sharedInstance,block 里的代碼只會被執行一次。
- 所以這里的 _instance = [[super allocWithZone:NULL] init]; 只會執行一次,保證單例唯一性。
- _instance = [[super allocWithZone:NULL] init];
- 真正創建對象的地方。
- 用 super allocWithZone:NULL 是為了繞過 allocWithZone: 的重寫(因為我們在類里也重寫了它,防止外部直接用 alloc 創建新對象)。
- 這樣寫就能保證只創建 一個實例對象。
推薦這個寫法的最根本的原因就是 dispatch_once 他會幫助你加鎖和判斷是否執行過的工作。
互斥鎖寫法(雙重檢查鎖)
這里我給出代碼以及會給出一些讀者可能的疑問的解答
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface LazyLockSingleton : NSObject<NSCopying, NSMutableCopying>
+ (instancetype) sharedInstance;
@property (nonatomic, strong) NSString *info;@endNS_ASSUME_NONNULL_END
#import "LazyLockSingleton.h"
static id _instance = nil;
@implementation LazyLockSingleton
+ (instancetype)sharedInstance {if (_instance == nil) {@synchronized (self) {if (_instance == nil) {_instance = [[super alloc] init];}}}return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {if (_instance == nil) {@synchronized (self) {if (_instance == nil) {_instance = [super allocWithZone:zone];}}}return _instance;
}
- (id) copyWithZone:(NSZone *)zone {return [LazyLockSingleton sharedInstance];
}
- (id) mutableCopyWithZone:(NSZone *)zone {return [LazyLockSingleton sharedInstance];
}
@end
這里可能會有這樣幾個問題會有疑惑
1.是否可以這樣創建單例
+ (instancetype)sharedInstance {if (_instance == nil) {@synchronized (self) {_instance = [[super alloc] init];}}return _instance;
}
實則不行,我們只是鎖住了對象的創建,如果兩個線程同時進入 if,那么就會產生兩個對象。
2.為什么要用 static
如果不用 static,其他的類中可以使用 extern 來拿到這個單例
extern id instance;
instance = nil;
如果其他類中對單例進行如下操作,那么單例就會被重新創建,我們原本的單例對象中的內容就被銷毀了。
2.餓漢模式
餓漢模式是一種單例模式實現方式,它在類加載的時候就創建好唯一的實例,保證整個程序運行期間全局只有一個對象。
它有幾個非常奇妙的特點
- 線程安全
- 因為實例在類加載階段就已經創建完成,不存在多線程同時創建的問題。
- 立即初始化
- 無論程序是否會用到這個對象,它都會在類加載時創建。
- 簡單易實現
- 只需一個靜態變量初始化即可,不需要加鎖或 dispatch_once。
缺點
- 如果實例比較大或創建開銷高,而程序又不一定會用到,可能浪費內存和啟動時間。
下面給出代碼
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface EagerSingleton : NSObject<NSCopying, NSMutableCopying>
+ (instancetype) sharedInstance;
@property (nonatomic, strong) NSString *info;
@endNS_ASSUME_NONNULL_END
//餓漢式寫法
#import "EagerSingleton.h"
static id _instance;
@implementation EagerSingleton
+ (void)load {_instance = [[self alloc] init];
}
+ (instancetype)sharedInstance {return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {if (!_instance) {_instance = [super allocWithZone:zone];}return _instance;
}
- (id) copyWithZone:(NSZone *)zone {return _instance;
}
- (id) mutableCopyWithZone:(NSZone *)zone {return _instance;
}
@end
在這段代碼中我個人認為
+ (void)load {_instance = [[self alloc] init];
}
這段代碼完全體現了餓漢和懶漢的區別,它直接寫了一個類方法來去創建了一個后面需要用到的對象。
總結
其實有一個很形象的說法來去闡述餓漢和懶漢的區別就是餓漢就是直接吃飽再去干活,而懶漢就是等到需要吃的時候再去吃和干活,
下面我來總結一下懶漢(GCD & 互斥鎖)和餓漢
懶漢式 + 互斥鎖(Mutex)
-
核心邏輯:使用 @synchronized(self) 對實例創建加鎖
-
創建時機:第一次調用 sharedInstance 時才創建
-
線程安全: 保證線程安全
-
優點:
- 按需創建(懶加載)
- 保證全局唯一
-
缺點:
每次訪問都加鎖,性能較低
-
使用場景:小型項目,理解單例原理
**懶漢式 + GCD (dispatch_once) **
- 核心邏輯:使用 dispatch_once 保證 block 只執行一次
- 創建時機:第一次調用 sharedInstance 時創建
- 線程安全: 系統保證線程安全
- 優點:
- 按需創建(懶加載)
- 高效,只有第一次創建加鎖
- 代碼簡潔、官方推薦
- 缺點:幾乎無明顯缺點
- 使用場景:iOS / macOS 官方標準單例實現
餓漢式
-
核心邏輯:在類加載階段就創建靜態實例
-
創建時機:類加載時(程序啟動)
-
線程安全: 天然線程安全
-
優點:
- 實現簡單
- 天然線程安全
-
缺點:
程序啟動時就創建對象,如果對象大或未使用,可能浪費資源
-
使用場景:必須全局存在的管理類,如配置管理器、日志管理器