原文:The Right Way To Write a Singleton --- KrakenDev
尽管我在上一篇文章中写到了管理状态的苦恼,但有时我们无法避免它。管理状态的一个例子是我们都很熟悉的东西------单例(Singleton)。我们在 Swift 中发现的问题是,有很多方法来实现它。但哪种才是正确的方法?在这篇文章中,我将向你展示单例的历史,然后向你展示在 Swift 中实现单例的正确方式。
如果你想查看在 Swift 中实现单例模式的正确方法,以及证明它的 "正确性",你可以滚动到文章的底部。:)
回忆之旅
Swift 是 Objective-C 的进化版本。在 Objective-C 中,我们就是这样实现单例的:
objectivec
@interface Kraken : NSObject
@end
@implementation Kraken
+ (instancetype)sharedInstance {
static Kraken *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[Kraken alloc] init];
});
return sharedInstance;
}
@end
现在我们已经解决了这个问题,我们可以看到一个单例的基本结构,让我们制定一些规则,以便我们理解我们正在看的东西。
DA SINGLETON, MAHN 的规则
关于单例,基本上有三件事要记住:
- 一个单例必须是唯一 (unique)的。这就是为什么它被称为单例。在它在应用程序中存活的生命周期内,只能有一个实例。单例的存在是为了给我们提供单一的全局状态。这样的例子有
NSNotificationCenter
、UIApplication
和NSUserDefaults
。 - 为了保持单例的唯一性,单例的初始化构造器需要是私有(private)的。这有助于防止其他对象自己创建你的单例类的实例。感谢所有向我指出这一点的人 :)
- 由于规则#1,为了在应用程序的整个生命周期中只有一个实例,这意味着它需要是**线程安全(thread-safe)的。当你想到这一点时,并发性真的很糟糕,但简单来说,如果单例在代码中构建不正确时,你可以让两个线程同时尝试初始化一个单例,这有可能给你返回一个单例的两个独立实例。这意味着它有可能不是唯一的,除非我们让它是 线程安全(thread-safe)**的。这意味着我们要把初始化封装在一个
dispatch_once
GCD 块中,以确保初始化代码在运行时只运行一次。
在应用程序中,在一个地方做唯一的初始化很容易。在这篇文章的其余部分中,需要记住的是,单例要满足更难看的 dispatch_once
规则。
Swift 单例
从 Swift 1.0 开始,就有几种方法来创建一个单例。这些方法在这里、这里和这里都有非常广泛的介绍。但谁会喜欢点击链接呢?剧透警报;有四种变化。请允许我细数一下这些方法。
最丑陋的方式(又称 "如果你只是要这样做,为什么还要用 Swift 编码" 的方式)
swift
class Singleton {
class var sharedInstance: Singleton {
struct Static {
static var onceToken: dispatch_once_t = 0
static var instance: Singleton? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = Singleton()
}
return Static.instance!
}
}
这种方式是将 Objective-C 的单例实现直接移植到 Swift 上。在我看来是很难看的,因为 Swift 本来就是要简洁明了,富有表现力。比移植的人要好。要更好。 :P
结构体方式(又称 "古老但奇怪的是仍然流行" 的方式)
swift
class Singleton {
class var sharedInstance: Singleton {
struct Static {
static let instance: Singleton = Singleton()
}
return Static.instance
}
}
这就是我们在 Swift 1.0 中不得不采取的方式,因为那时的类还不支持 static
类型的变量。然而,结构体却支持静态变量。由于这些对静态变量的限制,我们被迫采用了这样的模式,看起来就像上面这样。这比直接移植 Objective-C 好,但还是不够好。有趣的是,在 Swift 1.2 发布几个月后,我仍然看到这种写单例的方法。但后面会有更多的内容。
全局变量方式(也就是所谓的 "单线单例")。
swift
private let sharedInstance = SomeObject()
class SomeObject {
class var sharedInstance: SomeObject {
return sharedInstance
}
}
从 Swift 1.2 开始,我们有了 **访问控制说明符(access control specifiers)和静态类成员(static class members)**的能力。这意味着我们不必让全局变量扰乱全局命名空间,我们现在可以防止命名空间冲突。在我看来,这个版本的 Swiftier 很多。
现在,你可能会问为什么我们在结构体或全局变量的实现中没有看到 dispatch_once
。根据 Apple 的说法,这些方法都满足了我上面概述的 dispatch_once
条款。以下是直接从他们的 Swift 博客中引用的一段话,证明了他们在底层被封装在 dispatch_once
块中:
"The lazy initializer for a global variable (also for static members of structs and enums) is run the first time that global is accessed, and is launched as dispatch_once to make sure that the initialization is atomic. This enables a cool way to use dispatch_once in your code: just declare a global variable with an initializer and mark it private." --- Apple's Swift Blog
全局变量的惰性初始化器(也适用于结构体和枚举的静态成员)会在第一次访问该全局变量时运行,并以
dispatch_once
方式启动,以确保初始化是原子性的。这使得在你的代码中使用dispatch_once
成为一种很酷的方式:只要用初始化器声明一个全局变量并将其标记为private
。
就官方文档而言,这就是 Apple 给我们的全部。但这意味着我们所能证明的只是全局变量和结构体/枚举的静态成员。在这一点上,唯一有 Apple 文档支持的 100% 安全的赌注是使用全局变量来惰性地在dispatch_once
块中包裹单例初始化。但是我们的静态类变量怎么办?
这个问题把我们带到了下一个令人兴奋的部分:
正确的方法,又称 "一行代码实现单例(现在有证明了!")。
swift
class Singleton {
static let sharedInstance = Singleton()
}
所以我为这篇文章做了相当多的研究。事实上,这篇文章的灵感来自于我们今天在 Capital One 的一次谈话,因为我们审查了一个 PR,目的是在我们的应用中适当的实现 Swift 单例一致性。我们知道这种编写单例的 "正确" 方法,但除了推测,我们没有任何证据来支持我们的推理。在没有足够文档的情况下,试图支持这种方法是没有用的。这是我对互联网/博客圈中缺乏信息的说法。每个人都知道,如果它不在互联网上,就不是真的。这让我很难过。
我浏览了互联网的末端(也就是谷歌搜索结果的第10页),却一无所获。难道还没有人公布一行代码实现单列的证据吗?也许他们有,但很难找到。
所以我决定做点什么,写出了每一种初始化单例的方法,并在运行时使用断点来检查它们。在分析了每个堆栈跟踪的任何相似之处之后,我发现了一些有趣的东西--证明!这是我的一个想法。
看看吧,哟(哦,为班级的表情符号欢呼吧!)。
Using the Global Singleton
Using the One Line Singleton
第一张图片显示了一个全局 let
实例的堆栈跟踪。红色的部分是我们感兴趣的东西。在 Kraken 单例的实际初始化执行之前,有一个标记为 swift_once
的调用跟踪,后面是swift_once_block_invoke
调用。既然苹果公司说他们在 dispatch_once
块中惰性地实例化了全局变量,我们可以有把握地假设这就是他们的意思。
利用这些知识,我检查了我们闪亮而漂亮的一行代码实现单例的堆栈跟踪。正如你在第二张图片中所看到的,它是完全一样的! 所以你有了它! 证明了我们的一行代码实现单例是正确的。现在世界一切都好了。另外,既然这个帖子在互联网上出现了,那就一定意味着它是真的!
😉😉
不要忘了私有的初始化方法!
正如 Apple 的框架布道者 @davedelong 慷慨地指出的那样,你必须确保你的 inits 是 private
的。这可以确保你的单例是真正唯一的,并防止外部对象通过访问控制来创建自己的类的实例。因为在 Swift 中,所有对象都有一个默认的 public
初始化器,你需要覆写你的 init
方法并使其私有化。这并不难做到,而且还能保证我们的一行代码实现单例是漂亮的:
swift
class Singleton {
static let sharedInstance = Singleton()
private init() {} // 这样可以防止其他对象使用这个类的默认 '()' 初始化器。
}
这样做将确保当任何类试图使用 ()
初始化 Singleton
实例时,编译器会抛出这个错误:
就这样,你拥有了它! 完美的、一行代码实现的单例。
总结
与 jtbandes 在 Stack Overflow 上对 swift singletons 的顶级答案的精彩评论相呼应,我根本无法在任何地方找到通过 "virtur of let" 来证明线程安全的文档。实际上,我记得去年参加 WWDC 时说过类似的话,但你不能指望读者或快速的 Googlers 在试图确定这是 Swift 中写单例的正确方法时偶然发现。希望这篇文章能帮助外面的人理解为什么 Swift 中的一行代码实现单例是个好方法。
编码快乐,书呆子们!