公司动态
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写法如下:final class Manager { static let shared Manager() private init() {} }核心特征全局唯一整个进程中只存在一个实例私有构造外部无法直接创建新实例全局访问通过静态属性或方法获取唯一实例延迟加载首次访问时才创建实例Swift 中由 static 保证1.2 为什么需要单例在 iOS 开发中有些对象天然就应该是唯一的硬件资源屏幕、传感器、网络接口等物理资源只有一个共享状态用户登录信息、应用配置等需要全局共享的数据系统服务通知中心、文件管理、用户偏好等系统级服务单例的本质是共享状态的容器。它让不同模块之间无需显式传递即可访问同一份数据。二、iOS 中的单例写法2.1 Swift 中的标准写法Swift 中实现单例非常简洁使用static let即可保证线程安全和唯一性// 标准单例写法 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 中传统写法需要手动处理线程安全// 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 单例的变体全局常量对于简单的值类型数据可以直接使用全局常量效果等同于单例// 全局常量方式 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.sharedUIKit应用生命周期管理、打开 URL、设置状态栏UserDefaults.standardFoundation轻量级用户偏好存储FileManager.defaultFoundation文件系统操作NotificationCenter.defaultFoundation通知的发布与订阅URLSession.sharedFoundation基础网络请求ProcessInfo.processInfoFoundation进程环境信息、系统参数3.2 系统单例的使用示例// 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.禁止外部调用initOC禁止外部调用init方法如下// OC (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE;Swift禁止外部调用的方式如下// Swift private init() {}2.内部状态也要线程安全单例只有一个不代表线程安全内部数据竞争才是大头。OC示例代码如下// OC property (nonatomic, strong) NSMutableArray *items; // 读/写要加锁 - (void)addItem:(id)item { synchronized(self) { [_items addObject:item]; } }Swift使用actoractor DataStore { static let shared DataStore() private init() {} private var cache: [String: Any] [:] func setValue(_ value: Any, for key: String) { cache[key] value } }单例内部可变状态必须明确标注同步策略。2.架构设计1.单例只做协调者不做数据仓库错误示例// 上帝单例 UserManager.shared.currentUser UserManager.shared.login() UserManager.shared.uploadAvatar() UserManager.shared.trackEvent() UserManager.shared.clearCache()我们可以拆分成多个单例UserService.shared // 登录/用户数据 Analytics.shared // 埋点 FileUploader.shared // 文件上传 CacheManager.shared // 缓存每个单例职责单一接口收敛review 时一眼能看懂改了什么。2.对外暴露不可变接口// ❌ 可变谁都能改 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/ 文件头写清楚/// ⚠️ 线程安全所有方法必须在主线程调用 /// ⚠️ 禁止在 load / static initializer 中访问 /// ⚠️ 单元测试中请使用 reset() 清理状态 interface AudioEngine : NSObject (instancetype)sharedInstance; end3.协作流程层面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解耦使用协议解耦降低单例的传染性。// 定义协议 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 #if DEBUG extension UserManager { func resetForTesting() { currentUser nil // 清理其他状态 } } #endif每个测试用例前后 reset避免测试之间互相污染。4.Swift项目用 MainActor 约束主线程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 可测试的单例通过协议抽象可以让单例变得可测试// 定义协议便于 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() -gt; User? { // 读取逻辑 return nil } } // 使用时依赖协议而非具体单例 class ProfileViewController { private let storage: UserStorage init(storage: UserStorage UserManager.shared) { self.storage storage } }6.2 避免单例状态污染尽量减少单例中的可变状态将不可变配置与可变状态分离// 不好的设计单例中大量可变状态 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 开发中最基础也最实用的设计模式之一。它解决了全局唯一实例和全局访问两大核心问题在系统框架中无处不在。使用单例时需要牢记以下原则明确需求确认对象确实需要全局唯一而非仅仅为了方便控制状态尽量减少单例中的可变状态避免全局污染面向协议通过协议抽象提高可测试性谨慎使用单例不是万能药依赖注入等替代方案往往更优