一、核心概念
-
單例模式
一個(gè)類在應(yīng)用生命周期內(nèi)只創(chuàng)建唯一一個(gè)實(shí)例,提供全局統(tǒng)一訪問入口。 -
線程安全
多線程并發(fā)訪問時(shí),只創(chuàng)建一個(gè)實(shí)例,且屬性/方法讀寫不崩潰、數(shù)據(jù)不亂序。
二、單例模式 優(yōu)缺點(diǎn)(面試必背)
? 優(yōu)點(diǎn)
- 全局唯一:避免重復(fù)創(chuàng)建對象,節(jié)省內(nèi)存與資源
- 全局共享:跨頁面、跨模塊方便共享數(shù)據(jù)/狀態(tài)(用戶信息、網(wǎng)絡(luò)管理等)
- 統(tǒng)一管理:集中管理配置、資源、請求,便于維護(hù)
- 生命周期穩(wěn)定:程序運(yùn)行期間常駐內(nèi)存,不用頻繁創(chuàng)建銷毀
? 缺點(diǎn)
- 耦合度高:全局共享導(dǎo)致模塊強(qiáng)依賴,不利于解耦
- 測試?yán)щy:單例常駐內(nèi)存,單元測試數(shù)據(jù)無法隔離
- 內(nèi)存常駐:實(shí)例不會(huì)自動(dòng)釋放,過多單例會(huì)占用內(nèi)存
- 線程風(fēng)險(xiǎn):創(chuàng)建安全 ≠ 使用安全,可變屬性仍需加鎖
- 擴(kuò)展性差:不支持多實(shí)例,繼承與改造難度大
三、線程安全問題根源(錯(cuò)誤示范)
多線程并發(fā)時(shí)會(huì)創(chuàng)建多個(gè)實(shí)例,違背單例原則。
@implementation Singleton
+ (instancetype)sharedInstance {
static Singleton *instance = nil;
// 多線程可同時(shí)通過判斷,導(dǎo)致多次初始化
if (instance == nil) {
instance = [[self alloc] init];
}
return instance;
}
@end
問題核心:無線程同步,多線程同時(shí)判空,多次執(zhí)行 init。
四、生產(chǎn)級標(biāo)準(zhǔn)實(shí)現(xiàn)(官方推薦|絕對線程安全)
1. Objective-C 最優(yōu)實(shí)現(xiàn)(dispatch_once)
SafeSingleton.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SafeSingleton : NSObject <NSCopying, NSMutableCopying>
/// 單例全局訪問入口
+ (instancetype)sharedInstance;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
+ (instancetype)alloc NS_UNAVAILABLE;
+ (instancetype)allocWithZone:(struct _NSZone *)zone NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END
SafeSingleton.m
#import "SafeSingleton.h"
@implementation SafeSingleton
+ (instancetype)sharedInstance {
static SafeSingleton *instance = nil;
static dispatch_once_t onceToken;
// 蘋果官方線程安全方案,代碼塊只執(zhí)行一次
dispatch_once(&onceToken, ^{
instance = [[super allocWithZone:nil] init];
});
return instance;
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return self;
}
@end
2. Swift 最優(yōu)實(shí)現(xiàn)(static let)
import Foundation
class SafeSingleton: NSObject, NSCopying, NSMutableCopying {
/// 編譯器自動(dòng)保證線程安全
static let shared = SafeSingleton()
private override init() {
super.init()
}
func copy(with zone: NSZone? = nil) -> Any {
return self
}
func mutableCopy(with zone: NSZone? = nil) -> Any {
return self
}
@available(*, unavailable)
class func new() -> Self {
fatalError("禁止使用 new")
}
}
五、單例屬性/方法 線程安全(高頻面試點(diǎn))
重要結(jié)論:
實(shí)例創(chuàng)建線程安全 ≠ 業(yè)務(wù)方法線程安全!
單例中的可變屬性、讀寫操作必須加鎖!
DataManager(完整可運(yùn)行)
DataManager.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface DataManager : NSObject
+ (instancetype)sharedManager;
- (void)addData:(id)data;
- (void)removeDataAtIndex:(NSUInteger)index;
- (NSArray *)fetchAllData;
@end
NS_ASSUME_NONNULL_END
DataManager.m
#import "DataManager.h"
@interface DataManager ()
@property (nonatomic, strong) NSMutableArray *dataList;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
@end
@implementation DataManager
+ (instancetype)sharedManager {
static DataManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[super allocWithZone:nil] init];
instance->_dataList = [NSMutableArray array];
instance->_serialQueue = dispatch_queue_create("com.example.DataManager", DISPATCH_QUEUE_SERIAL);
});
return instance;
}
- (void)addData:(id)data {
if (!data) return;
dispatch_sync(self.serialQueue, ^{
[self.dataList addObject:data];
});
}
- (void)removeDataAtIndex:(NSUInteger)index {
dispatch_sync(self.serialQueue, ^{
if (index < self.dataList.count) {
[self.dataList removeObjectAtIndex:index];
}
});
}
- (NSArray *)fetchAllData {
__block NSArray *result;
dispatch_sync(self.serialQueue, ^{
result = [self.dataList copy];
});
return result;
}
@end
六、鎖實(shí)現(xiàn)方案(兼容舊版,不推薦)
1. @synchronized
@implementation LockSingleton
+ (instancetype)sharedInstance {
static LockSingleton *instance = nil;
@synchronized (self) {
if (instance == nil) {
instance = [[super allocWithZone:nil] init];
}
}
return instance;
}
@end
2. pthread_mutex
#import <pthread/pthread.h>
@implementation PthreadSingleton
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+ (instancetype)sharedInstance {
static PthreadSingleton *instance = nil;
pthread_mutex_lock(&mutex);
if (instance == nil) {
instance = [[super allocWithZone:nil] init];
}
pthread_mutex_unlock(&mutex);
return instance;
}
@end
七、iOS 單例 線程安全 高頻面試題(完整版)
1. 為什么普通單例不是線程安全的?
- 多線程同時(shí)進(jìn)入
if (instance == nil)分支 - 多個(gè)線程同時(shí)執(zhí)行
[[self alloc] init] - 最終生成多個(gè)實(shí)例,破壞單例唯一性
2. iOS 中如何實(shí)現(xiàn)線程安全的單例?
-
OC:使用
dispatch_once(官方首選) -
Swift:使用
static let(編譯器底層等價(jià) dispatch_once) - 不推薦:
@synchronized、NSLock等鎖方案(性能差)
3. dispatch_once 為什么線程安全?
- 底層基于CPU 原子指令實(shí)現(xiàn)
- 無鎖設(shè)計(jì),只在第一次執(zhí)行初始化
- 后續(xù)線程直接讀取結(jié)果,性能極高
- 保證代碼塊一生只執(zhí)行一次
4. Swift 單例為什么用 static let 就線程安全?
- Swift 編譯器對
static let做了安全優(yōu)化 - 底層自動(dòng)插入
dispatch_once邏輯 - 保證初始化原子性、唯一性
- 懶加載 + 線程安全,一步到位
5. 單例創(chuàng)建線程安全,就代表所有方法都安全嗎?
不是。
- 單例創(chuàng)建安全只保證實(shí)例唯一
- 內(nèi)部可變對象(NSMutableArray、NSMutableDictionary)仍非線程安全
- 多線程同時(shí)讀寫會(huì)導(dǎo)致崩潰、數(shù)據(jù)錯(cuò)亂
- 解決方案:加鎖 / GCD 串行隊(duì)列
6. 為什么不推薦用雙重檢查鎖(DCL)?
- OC 內(nèi)存模型不保證
static變量賦值的原子性 - 可能出現(xiàn)半初始化對象,導(dǎo)致野指針、崩潰
- 蘋果官方明確不推薦,存在高風(fēng)險(xiǎn)
7. 如何防止別人通過 alloc / init / copy 創(chuàng)建新實(shí)例?
- OC:
NS_UNAVAILABLE禁用init/new/alloc - OC:重寫
copyWithZone/mutableCopyWithZone返回 self - Swift:
private init()私有化構(gòu)造方法
8. 單例的生命周期?
- 單例被靜態(tài)變量持有,存儲(chǔ)在靜態(tài)存儲(chǔ)區(qū)
- 生命周期伴隨整個(gè) App 進(jìn)程
- App 退出前不會(huì)釋放
- 無法手動(dòng)銷毀,慎用大量單例
9. 單例好還是全局變量好?
- 單例:封裝性好、可控、支持延遲初始化、線程安全易實(shí)現(xiàn)
- 全局變量:無封裝、污染全局作用域、不安全
- 開發(fā)中優(yōu)先單例
10. 你在項(xiàng)目中怎么使用單例?
常用場景:
- 用戶信息管理(UserManager)
- 網(wǎng)絡(luò)請求管理(NetworkManager)
- 音頻/播放器管理
- 全局配置管理
- 數(shù)據(jù)庫管理
八、避坑總結(jié)
- OC 首選
dispatch_once,Swift 首選static let - 禁止裸判空實(shí)現(xiàn)單例
- 禁止雙重檢查鎖
- 可變屬性必須用串行隊(duì)列/鎖保護(hù)
- 禁止外部手動(dòng)創(chuàng)建實(shí)例
- 單例不要濫用,避免內(nèi)存與耦合問題
九、最終一句話總結(jié)
- 創(chuàng)建安全:OC = dispatch_once,Swift = static let
- 使用安全:可變屬性讀寫必須串行化
- 面試核心:線程安全原理、dispatch_once 機(jī)制、單例優(yōu)缺點(diǎn)、可變對象安全處理