Singleton(单例)—— 一种创建型模式,确保类只有一个实例并提供全局访问点。Singleton 广泛应用于移动开发中的共享资源:网络客户端、数据库、设置管理器。该模式在经典著作 GoF(1994)中有所描述,至今仍是最广为人知的模式之一。更多详情请参阅 Refactoring Guru:Singleton。
要点
Singleton(单例)—— 由 GoF(Gang of Four)于 1994 年描述的创建型设计模式。该模式解决两个问题:将类实例的创建限制为一个对象,并提供对该对象的全局访问。Singleton 对于必须唯一的资源非常有用:会话工厂、图像缓存、数据库连接管理器、Crashlytics 或 Analytics 客户端。
Singleton 的实现 需要私有构造函数(禁止外部创建)、包含唯一实例的静态字段和静态访问方法(shared、instance、getInstance)。客户端调用 Singleton.shared.method(),无需关心对象的创建。该模式在 iOS 和 Android 中很流行:URLSession.shared、UserDefaults.standard、FirebaseApp.sharedInstance — 这些都是 Singleton。然而,过度使用 Singleton 会导致 Global State 反模式。
Singleton 的问题 — 隐藏依赖(类隐式依赖于 Singleton 对象)、测试困难(在测试中无法在不额外努力的情况下替换实例)、违反单一职责原则(Singleton 同时管理自身实例和业务逻辑)。现代移动开发更倾向于使用 DI(Dagger、Hilt、Swinject)来管理唯一实例 — DI 容器创建一次对象并通过构造函数注入。
Swift Singleton 通过具有私有初始化器的静态属性 shared 实现。从 Swift 3 开始,静态属性的延迟初始化保证是线程安全的 — 编译器通过 dispatch_once 自动添加同步。只需声明 static let shared = Class() 并将 init() 设为私有即可。Swift 在初始化后不需要额外的同步来支持单线程访问。
final class NetworkManager {
// Thread-safe Singleton
static let shared = NetworkManager()
private init() {
URLSessionConfiguration.default.timeoutIntervalForRequest = 30
}
private var cache = NSCache<NSString, NSData>()
func fetchData(from url: URL) async throws -> Data {
let key = url.absoluteString as NSString
if let cached = cache.object(forKey: key) {
return cached as Data
}
let (data, _) = try await URLSession.shared.data(from: url)
cache.setObject(data as NSData, forKey: key)
return data
}
}
// 使用
let data = try await NetworkManager.shared.fetchData(from: url)
Apple Singleton — 在 iOS SDK 中,许多对象使用 Singleton:UIApplication.shared、UIScreen.main、FileManager.default、NotificationCenter.default、UserDefaults.standard。Apple 将 Singleton 用于物理上唯一的服务(一个屏幕、一个应用程序)。开发人员将此模式复制到自己的服务中。在 SwiftUI 中,对 Singleton 的全局访问被 Environment 和 @EnvironmentObject 取代,从而提高了可测试性。
Kotlin Singleton — 最简单的方式:关键字 object 声明一个单例类,在首次访问时进行延迟初始化。Kotlin object 是线程安全的,不需要额外的同步。如果需要带构造函数参数的 Singleton,则使用带有 lazy 委托的 companion object。在 Android 中,Singleton 通常用于 Application 上下文和通过 Application.onCreate() 初始化的服务。
// 选项 1:object — 不带参数的简单 Singleton
object AppPreferences {
private val prefs = Application.instance
.getSharedPreferences("app", Context.MODE_PRIVATE)
var isFirstLaunch: Boolean
get() = prefs.getBoolean("first_launch", true)
set(value) = prefs.edit { putBoolean("first_launch", value) }
}
// 选项 2:companion object — 带参数的 Singleton
class ApiClient private constructor(baseUrl: String) {
companion object {
@Volatile
private var instance: ApiClient? = null
fun getInstance(baseUrl: String): ApiClient {
return instance ?: this.synchronized {
instance ?: ApiClient(baseUrl).also { instance = it }
}
}
}
fun request(endpoint: String): String { /* ... */ }
}
Android SDK Singleton — 许多 Android 系统服务实现了 Singleton:context.getSystemService()、Room.databaseBuilder()、Retrofit.Builder()。示例包括 SharedPreferences、MediaPlayer、AudioManager。在 Android 应用程序中,Singleton 通常用于存储库、管理器和工厂。Google 建议用 DI(Hilt、Koin)替代 Singleton,其中 Singleton 作用域(Scope.Singleton 或 @Singleton)由容器管理,类保持可测试性。
线程安全 — 在多线程环境中对 Singleton 的关键要求。没有同步,两个线程可能同时检查 instance == null 并创建两个实例。解决方案 — 在首次创建时加锁,初始化后解锁。在 Swift 中,静态属性(static let)默认是线程安全的。在 Kotlin 中,object 是线程安全的。对于 Kotlin 中的 Java 风格,使用 synchronized 或 @Volatile + double-check locking。
| 语言 | 机制 | 线程安全 | 延迟初始化 |
|---|---|---|---|
| Swift | static let | dispatch_once(自动) | 是,首次访问时 |
| Kotlin object | Object declaration | 类初始化器线程安全 | 是,首次访问时 |
| Kotlin companion | synchronized + @Volatile | 双重检查锁定 | 是,通过 lazy 或 synchronized |
| Java | synchronized + volatile | 双重检查锁定 | 是,在 getInstance() 中 |
双重检查锁定 — 用于延迟初始化 Singleton 的模式。第一次检查不带同步(如果实例已存在则快速),第二次 — 在 synchronized 内部(仅由一个线程创建)。@Volatile 保证所有线程对更改的可见性。没有 volatile,其他线程可能会看到部分创建的对象。在 Kotlin 中,使用 LazyThreadSafetyMode.SYNCHRONIZED 的 lazy 委托自动实现双重检查锁定。
依赖注入 — 管理唯一实例的 Singleton 替代方案。DI 容器(Dagger、Hilt、Koin、Swinject)在 Singleton 作用域中创建一次对象并通过构造函数注入。类不知道自己的 Singleton 状态 — 这由容器决定。代码变得可测试:在测试中,DI 模块被 mock 模块替换。DI 的优点:构造函数中的显式依赖、可覆写性、统一的生命周期。
何时 Singleton 是合理的 — 系统级对象:Crashlytics、Analytics、Logging。这些服务在 AppDelegate/Application 中初始化一次,并在各处使用。DI 对它们来说过于复杂。Singleton 也适用于图像缓存(NSCache、Coil、Glide),其中全局访问因性能而合理。对于其他所有情况,DI 更可取:它使依赖关系可见,简化了测试和重构。
混合方法 — 具有测试覆写能力的 Singleton。在 Swift 中,使用协议 + 静态属性,测试可以替换该属性(例如,通过 URLProtocol 替换 URLSession)。在 Kotlin 中 — 具有可注入属性的开放类,测试通过反射或 setter 设置 mock。这种方法保持了 Singleton 的简单性,同时提供了测试工具。Google 为 Android 推荐 Hilt,Apple 不为 iOS 强制使用 DI — 选择取决于团队。
常见问题
不,Singleton 是 GoF 模式,但其经常性的错误使用使其变成了 Global State 反模式。Singleton 对于物理上唯一的资源(屏幕、打印机、文件系统)是合理的。当 Singleton 用于管理数据时会出现问题:隐藏依赖、测试困难、违反单一职责原则。现代替代方案 — 具有 Singleton 作用域的 DI。
三种方法:(1)通过协议 — Singleton 实现协议,测试替换实现;(2)通过 DI — Singleton 作为依赖通过构造函数注入;(3)通过重置方法 — Singleton 具有在测试中重置状态的方法(仅用于测试构建)。第一种方法更可取,第三种 — 对生产环境危险。Swift 允许在测试中通过运行时操作替换 shared 属性。
Kotlin object — 一种语言构造,在字节码级别创建 Singleton。与具有私有构造函数和 getInstance() 的 Java 实现不同,object 保证线程安全、延迟初始化和禁止继承。Java Singleton 需要手动同步(synchronized)和 volatile 才能在多线程环境中正确运行。Kotlin object — Android 中最安全、最简洁的方式。
继承 Singleton 会破坏该模式:如果 Singleton 类可以被继承,子类可以创建第二个实例,破坏唯一性。在 Swift 中,final class 禁止继承。Kotlin object 不能被继承(object 是密封的)。如果需要具有变体的 Singleton,请使用具有 Singleton 作用域的 DI 容器:它保证一个实例并通过接口支持继承。
参数通过 init(context: Application) 或 getInstance(param) 传递。Kotlin object 不接受参数 — 使用带有工厂方法 getInstance(param) 的 companion object。Hilt 解决了这个问题:@Singleton + @Inject constructor(context: Application) — DI 容器自动注入 Application 上下文。对于 retrofit 客户端,参数(baseUrl、interceptors)通过 DI 模块中的 builder 传递。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。