iOS中的单例模式详解

在 iOS 开发中,单例模式是最常用也最容易被误用的设计模式之一。它保证一个类只有一个实例,并提供一个全局访问点。从 UIApplication 到 UserDefaults,从 FileManager 到 NotificationCenter,系统框架中处处可见单例的身影。理解单例模式的本质、正确写法与潜在风险,是每一位 iOS 开发者进阶的必修课。

一、什么是单例模式

1.1 单例模式的定义

单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类在整个应用生命周期中只有一个实例,并提供一个全局访问点来获取该实例。

OC:

复制代码
+ (instancetype)sharedInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

Swift写法如下:

swift 复制代码
final class Manager {
    static let shared = Manager()
    private init() {}
}

核心特征

  • 全局唯一:整个进程中只存在一个实例
  • 私有构造:外部无法直接创建新实例
  • 全局访问:通过静态属性或方法获取唯一实例
  • 延迟加载:首次访问时才创建实例(Swift 中由 static 保证)

1.2 为什么需要单例

在 iOS 开发中,有些对象天然就应该是唯一的:

  • 硬件资源:屏幕、传感器、网络接口等物理资源只有一个

  • 共享状态:用户登录信息、应用配置等需要全局共享的数据

  • 系统服务:通知中心、文件管理、用户偏好等系统级服务

单例的本质是共享状态的容器。它让不同模块之间无需显式传递,即可访问同一份数据。

二、iOS 中的单例写法

2.1 Swift 中的标准写法

Swift 中实现单例非常简洁,使用 static let 即可保证线程安全和唯一性:

swift 复制代码
// 标准单例写法
class NetworkManager {
    // 静态常量,系统保证只初始化一次,且线程安全
    static let shared = NetworkManager()
// 私有化构造方法,防止外部创建新实例
private init() {
    // 初始化配置
}

func request(url: URL) {
    // 网络请求逻辑
}
}
// 使用方式
NetworkManager.shared.request(url: url)

为什么static let是线程安全的?

Swift 的 static let 本质上是全局变量,编译器会为其生成 dispatch_once 风格的初始化代码,保证在多线程环境下也只会初始化一次。

2.2 Objective-C 中的标准写法

在 Objective-C 中,传统写法需要手动处理线程安全:

objc 复制代码
// Objective-C 单例写法

@implementation IFLYColorManager

- (instancetype)sharedInstance{
    static IFLYColorManager * manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[IFLYColorManager alloc]init];
    });
    return manager;
}

@end

+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    static IFLYColorManager * manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [super allocWithZone:zone];
    });
    return manager;
}
- (instancetype)init{
    self = [super init];
    if (self) {
        //初始化配置
    }
    return self;
}

dispatch_once 的作用

  • 保证代码块在整个生命周期中只执行一次
  • 天然线程安全,无需加锁
  • 性能极高,首次调用后开销几乎为零

2.3 单例的变体:全局常量

对于简单的值类型数据,可以直接使用全局常量,效果等同于单例:

swift 复制代码
// 全局常量方式
struct AppConfig {
    static let appName = "MyApp"
    static let version = "1.0.0"
    static let apiBaseURL = URL(string: "https://api.example.com")!
}
// 使用方式
print(AppConfig.appName)

三、系统框架中的单例

3.1 常见的系统单例

iOS 系统框架中大量使用了单例模式,以下是最常见的几个:

系统单例 所属框架 用途
UIApplication.shared UIKit 应用生命周期管理、打开 URL、设置状态栏
UserDefaults.standard Foundation 轻量级用户偏好存储
FileManager.default Foundation 文件系统操作
NotificationCenter.default Foundation 通知的发布与订阅
URLSession.shared Foundation 基础网络请求
ProcessInfo.processInfo Foundation 进程环境信息、系统参数

3.2 系统单例的使用示例

swift 复制代码
// UIApplication:获取当前应用
let app = UIApplication.shared
// UserDefaults:读写用户偏好
UserDefaults.standard.set("John", forKey: "username")
let name = UserDefaults.standard.string(forKey: "username")
// FileManager:获取文档目录
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
// NotificationCenter:发送通知
NotificationCenter.default.post(name: .init("UserDidLogin"), object: nil)
// URLSession:发起网络请求
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// 处理响应
}

四、单例的优缺点

4.1 单例的优点

  • 全局访问方便:任何地方都能直接获取实例,无需层层传递
  • 节省资源:实例只创建一次,避免重复创建的开销
  • 状态共享:天然适合存放全局共享的数据
  • 线程安全:Swift 的 static let 和 dispatch_once 都保证了线程安全

4.2 单例的缺点

  • 全局状态污染:任何地方都能修改单例状态,导致问题难以追踪
  • 难以测试:单例的全局性使得单元测试难以隔离和 mock
  • 隐藏依赖:代码中直接使用单例,导致模块间耦合度升高
  • 生命周期不可控:单例一旦创建,直到应用结束才会释放

单例是一把双刃剑。用得好,它是简洁高效的全局状态管理方案;用得不好,它会成为代码腐化的温床。

五、多人协作如何防止单例滥用

当我们多人协作使用单例的时候,很容易出现随意读写单例的情况,这里我们介绍几种避免这种情况的方法。

1.代码层面

1.禁止外部调用init

OC禁止外部调用init方法如下:

复制代码
// OC
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;

Swift禁止外部调用的方式如下:

Swift 复制代码
// Swift
private init() {}

2.内部状态也要线程安全

单例"只有一个"不代表"线程安全",内部数据竞争才是大头。

OC示例代码如下:

Swift 复制代码
// OC
@property (nonatomic, strong) NSMutableArray *items;

// 读/写要加锁
- (void)addItem:(id)item {
    @synchronized(self) {
        [_items addObject:item];
    }
}

Swift使用actor

Swift 复制代码
actor DataStore {
    static let shared = DataStore()
    private init() {}
    
    private var cache: [String: Any] = [:]
    
    func setValue(_ value: Any, for key: String) {
        cache[key] = value
    }
}

单例内部可变状态,必须明确标注同步策略。

2.架构设计

1.单例只做"协调者",不做"数据仓库"

错误示例:

Swift 复制代码
// 上帝单例
UserManager.shared.currentUser
UserManager.shared.login()
UserManager.shared.uploadAvatar()
UserManager.shared.trackEvent()
UserManager.shared.clearCache()

我们可以拆分成多个单例:

Swift 复制代码
UserService.shared      // 登录/用户数据
Analytics.shared        // 埋点
FileUploader.shared     // 文件上传
CacheManager.shared     // 缓存

每个单例职责单一,接口收敛,review 时一眼能看懂改了什么。

2.对外暴露不可变接口

Swift 复制代码
// ❌ 可变,谁都能改
class UserManager {
    static let shared = UserManager()
    var currentUser: User?
}

// ✅ 只读 + 方法修改
class UserManager {
    static let shared = UserManager()
    private(set) var currentUser: User?
    
    func updateUser(_ user: User) {
        currentUser = user
        // 可以加通知、持久化、校验等统一逻辑
    }
}

这样所有修改都经过一个入口,方便加日志、断言、线程检查。

3.关键单例加"使用约束"注释/文档

.h / 文件头写清楚:

Swift 复制代码
/// ⚠️ 线程安全:所有方法必须在主线程调用
/// ⚠️ 禁止在 +load / static initializer 中访问
/// ⚠️ 单元测试中请使用 reset() 清理状态
@interface AudioEngine : NSObject
+ (instancetype)sharedInstance;
@end

3.协作流程层面

1.Code Review红线

在团队Code Review规范里明确列出:

|---------------------------------------------------------------------|----------------------------------------|
| 红线 | 说明 |
| | ❌ 禁止手写 DCL | 必须用 `dispatch_once` / `static let` | | 必须用 dispatch_once / static let |
| | ❌ 禁止暴露 `init` | 必须 `NS_UNAVAILABLE` / `private init()` | | 必须 NS_UNAVAILABLE / private init() |
| | ❌ 禁止单例持有强引用 ViewController | 会造成循环引用 | | 会造成循环引用 |
| | ❌ 禁止在单例里做耗时操作 | 阻塞初始化 | | 阻塞初始化 |
| | ❌ 禁止跨模块直接依赖单例内部实现 | 用协议隔离 | | 用协议隔离 |

2.用协议(Protocol)解耦,

使用协议解耦,降低单例的"传染性"。

Swift 复制代码
// 定义协议
protocol UserProviding {
    var currentUser: User? { get }
    func updateUser(_ user: User)
}

// 单例遵守协议
extension UserManager: UserProviding {}

// 业务代码依赖协议,不依赖单例
class ProfileViewModel {
    private let userProvider: UserProviding
    
    init(userProvider: UserProviding = UserManager.shared) {
        self.userProvider = userProvider
    }
}

好处:

• 单例可以被替换(Mock 测试)

• 新人看代码不用追到单例内部

• 以后想去掉单例也方便

3.单元测试

单例状态必须可重置

Swift 复制代码
// Swift
#if DEBUG
extension UserManager {
    func resetForTesting() {
        currentUser = nil
        // 清理其他状态
    }
}
#endif

每个测试用例前后 reset,避免测试之间互相污染。

4.Swift项目

用 `@MainActor` 约束主线程

Swift 复制代码
@MainActor
final class NavigationManager {
    static let shared = NavigationManager()
    private init() {}
    
    func push(_ vc: UIViewController) {
        // 编译器保证在主线程调用
    }
}

编译期就拦住线程错误,比运行时崩溃好 100 倍。

5.静态分析 + Lint 规则

• SwiftLint:自定义规则禁止 `static var`(强制 `static let`)

• Clang Static Analyzer:能抓到部分 DCL 问题

• Pre-commit Hook:检测新增单例是否符合模板

六、单例的进阶实践

6.1 可测试的单例

通过协议抽象,可以让单例变得可测试:

swift 复制代码
// 定义协议,便于 mock
protocol UserStorage {
    func saveUser(_ user: User)
    func loadUser() -> User?
}
// 单例实现协议
class UserManager: UserStorage {
static let shared = UserManager()
private init() {}
func saveUser(_ user: User) {
    // 存储逻辑
}

func loadUser() -> User? {
    // 读取逻辑
    return nil
}
}
// 使用时依赖协议而非具体单例
class ProfileViewController {
private let storage: UserStorage
init(storage: UserStorage = UserManager.shared) {
    self.storage = storage
}
}

6.2 避免单例状态污染

尽量减少单例中的可变状态,将不可变配置与可变状态分离:

swift 复制代码
// 不好的设计:单例中大量可变状态
class AppState {
    static let shared = AppState()
    private init() {}
var isLoggedIn = false
var username: String?
var userID: String?
var token: String?
// ... 越来越多的可变状态
}
// 更好的设计:不可变配置 + 独立状态管理
struct AppConfig {
static let apiBaseURL = URL(string: "https://api.example.com")!
static let appName = "MyApp"
}
// 用户状态单独管理,不放在单例中
class SessionManager {
private(set) var currentUser: User?
func login(user: User) {
    currentUser = user
}

func logout() {
    currentUser = nil
}
}

七、总结

单例模式是 iOS 开发中最基础也最实用的设计模式之一。它解决了全局唯一实例全局访问两大核心问题,在系统框架中无处不在。

使用单例时,需要牢记以下原则:

  • 明确需求:确认对象确实需要全局唯一,而非仅仅为了方便
  • 控制状态:尽量减少单例中的可变状态,避免全局污染
  • 面向协议:通过协议抽象提高可测试性
  • 谨慎使用:单例不是万能药,依赖注入等替代方案往往更优
相关推荐
ilawsonlu5 小时前
如何正确处理 CoreBluetooth 超时与 Task Cancellation
ios
00后程序员张5 小时前
SSL Pinning 抓包抓不到明文?绕过证书固定的几种方案
网络协议·计算机网络·网络安全·ios·adb·https·udp
weixin_531670896 小时前
Interlude起来:Swift原生Mac软件有什么优势?原生Mac应用和Electron应用有什么区别?
人工智能·macos·mac·swift
k4m7v2pz6 小时前
Swift Package Manager 在 macOS 26 上的三个编译错误排查指南
macos·spm·ai编程·xcode·swift·命令行工具
ii_best7 小时前
手机自动化脚本按键精灵实战:随机布局安全数字键盘的自动化输入方案
android·运维·ios·自动化·手机
cypking9 小时前
Objective-C 语法完整学习手册(小白自学 + 开发备查)
c语言·开发语言·学习·objective-c
软泡芙1 天前
【IOS】Codable
ios·ssh·cocoa
nvvas1 天前
9月苹果发布会前瞻:新iPhone、Apple Watch重点速览
ios·iphone
Zender Han1 天前
Flutter 自适应(Adaptive)与响应式(Responsive)设计实践:官方推荐方案详解
android·flutter·ios