Swift中单例模式详解

在之前的帖子里聊过状态管理有多痛苦,有时这是不可避免的。一个状态管理的例子大家都很熟悉,那就是单例。使用Swift时,有许多方法实现单例,这是个麻烦事,因为我们不知道哪个最合适。这里我们来回顾一下单例的历史,看一看在Swift中如何正确地实现单例。

如果你想直接看看Swift中单例的正确实现方式,直接跳到帖子最后即可。

往事回忆之ObjC单例

Swift是Objective-C的一种自然演变,它用如下的方式实现单例:

@interface Kraken : NSObject

@end

@implementation Kraken

+ (instancetype)sharedInstance {

static Kraken *sharedInstance = nil;

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

sharedInstance = [[Kraken alloc] init];

});

returnsharedInstance;

}

@end

在这个现成方案中,我们可以看到单例的基本结构。让我们来约定一些规则,这样便于更好的理解。

单例规则

关于单例,有三个重要的准则需要牢记:

1. 单例必须是唯一的(要不怎么叫单例?) 在程序生命周期中只能存在一个这样的实例。单例的存在使我们可以全局访问状态。例如:

NSNotificationCenter, UIApplication和NSUserDefaults。

2. 为保证单例的唯一性,单例类的初始化方法必须是私有的。这样就可以避免其他对象通过单例类创建额外的实例。

3. 考虑到规则1,为保证在整个程序的生命周期中值有一个实例被创建,单例必须是线程安全的。并发有时候确实挺复杂,简单说来,如果单例的代码不正确,如果有两个线程同时实例化一个单例对象,就可能会创建出两个单例对象。也就是说,必须保证单例的线程安全性,才可以保证其唯一性。通过调用dispatch_once,即可保证实例化代码只运行一次。

在程序中保持单例的唯一性,只初始化一次,这样并不难。帖子的余下部分中,需要记住:单例实现要满足隐藏的dispatch_once规则。

Swift单例

自Swift 1.0开始,创建单例有很多种方法。这些链接中已经有很详尽的描述,比如

https://github.com/hpique/SwiftSingletonhttp://stackoverflow.com/questions/24024549/dispatch-once-singleton-model-in-swift

https://developer.apple.com/swift/blog/?id=7。但是谁喜欢点链接呢?先剧透一下吧:总共有4个版本。我们来清点一下:

1. 最丑陋方法(Swift皮,Objective-C心)

class TheOneAndOnlyKraken {

classvarsharedInstance: TheOneAndOnlyKraken {

struct Static {

staticvaronceToken: dispatch_once_t = 0

staticvarinstance: TheOneAndOnlyKraken? = nil

}

dispatch_once(&Static.onceToken) {

Static.instance = TheOneAndOnlyKraken()

}

returnStatic.instance!

}

}

这个版本是Objective-C的直接移植版。我认为它不好看是因为Swift本该更简洁、更有描述力。不要做个搬运工,要做就做的更好。

2. 结构体方法(“新瓶装老酒)


Swift 1.0时,不支持静态类变量,那时这个方法是不得已而为之。但使用结构体,就可以支持这个功能。因为静态变量的限制,我们被约束在这样的一个模型中。这比Objective-C移植版本好一些,但还不够好。有趣的是,在Swift 1.2发布几个月后,我还可以看到这种写法。在那之后,反而更多了。

3.全局变量方法(“单行单例”方法)

private let sharedKraken = TheOneAndOnlyKraken()

class TheOneAndOnlyKraken {

classvarsharedInstance: TheOneAndOnlyKraken {

return   sharedKraken

         }

 }

在Swift 1.2以后,我们有了访问权限设置(access control specifiers) 的功能和静态类成员(static class members)。这意味着我们终于可以摆脱混乱的全局变量、全局命名空间,也不会发生命名空间冲突了。这个版本看起来更Swiftier一点。

现在,你可能会有疑问:为何看不到dispatch_once?根据Apple Swift博客中的说法,以上方法都自动满足dispatch_once规则。这里有个帖子可以证明dispatch_once规则一直在起作用。

“全局变量(还有结构体和枚举体的静态成员)的Lazy初始化方法会在其被访问的时候调用一次。类似于调用'dispatch_once'以保证其初始化的原子性。这样就有了一种很酷的'单次调用'方式:只声明一个全局变量和私有的初始化方法即可。”--来自Apple's Swift Blog

(“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官方文档给我们的所有信息,但这些已经足够证明全局变量和结构体/枚举体的静态成员是支持”dispatch_once”特性的。现在,我们相信使用全局变量来“懒包装”单例的初始化方法到dispatch_once代码块中是100%安全的。但是对于静态类变量来说,情况又如何?

这个问题带我们到更激动人心的思考中去:

正确的方法(也即是“单行单例法”)现在已经被证明正确。

class TheOneAndOnlyKraken {

static let sharedInstance = TheOneAndOnlyKraken()

}

到此为止,我们已经做了许多研究工作。这个帖子的灵感来源于我们在Capital One的一次对话:结对编程review代码的过程中,我们试图找到在App中使用Swift编写正确、一致的单例方法。我们知道编写单例的正确方法,但是无法用理论来证明。没有足够的文档支持,想证明方法的正确是徒劳的。在网上或博客圈中没有足够多的信息的话,这只能是一家之言,大家都知道如果网上查不到信息,就不会相信。这点让我很难过。

我搜索了许多信息,甚至翻到了google搜索结果的10多页,还是一无所获。难道没有人发帖证明单行单利方法的正确性?可能有人发过,但是太难被发现了。

因此我决定将各种单例都写一变,然后在运行时加入断点来观测。

分析了每个stack trace的记录后,我发现了有趣的东西——证据!

来看看截图:

使用全局单例方法

使用单行单例方法

第一张图片展示了使用全局实例时的stack trace。标红的地方需要注意。在调用Kraken单例之前,先调用了swift_once,接下来是swift_once_block_invoke。Apple之前在文档中已经说过,“懒实例化”的全局变量会被自动放在dispatch_once块中,我们可以假定说的就是这个东西。

了解了这些知识,我们来看看漂亮的单行单例方法。如图所示,调用完全一样。这样,我们就有了证据证明单行单例方法是正确的。

不要忘记设置初始化方法为私有

@davedelong,Apple的Framework传道者,善意地提醒我:必须保证init方法的私有性,只有这样,才能保证单例是真正唯一的,避免外部对象通过访问init方法创建单例类的其他实例。由于Swift中的所有对象都是由公共的初始化方法创建的,我们需要重写自己的init方法,并设置其为私有的。这很简单,而且不会破坏到我们优雅的单行单例方法。

class TheOneAndOnlyKraken {

static let sharedInstance = TheOneAndOnlyKraken()

private init() {}//This prevents others from using the default '()' initializer for this class.

}

这样做就可以保证编译器在某个类尝试使用()来初始化TheOneAndOnlyKraken时,抛出错误:

就是这样,我们的单行单例,非常完美!

最近又学到了将单例类置空的方法,与大家分享一下,代码如下:

class AppManager {

private static var _sharedInstance: AppManager?

class func getSharedInstance() -> AppManager {

guard let instance = _sharedInstance else {

_sharedInstance = AppManager()

return _sharedInstance!

}

return instance

}

private init() {} //私有化init方法

//销毁单例对象

class func destroy() {

_sharedInstance = nil

}

}

结论

这里回复一下jtbandes在“top rated answer to swift singletons on Stack Overflow”这个帖子中的问题:我也找不到哪里有文档证明let语句可以带来线程安全性的好处。我记得在去年参加WWDC的时候有类似的说法,没办法保证读者或各位Googler也偶遇到这个说法。希望这个帖子能帮助大家理解为什么单行单例在Swift中是正确的方法。

                                                                                                                          原文地址:http://www.cocoachina.com/swift/20151207/14584.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 在使用swift编程语言进行iOS应用开发的时候,我们常常借助单例来进行状态管理,但由于实现单例的方法很多,问题就...
    突然自我阅读 575评论 0 0
  • 在之前的帖子里聊过状态管理有多痛苦,有时这是不可避免的。一个状态管理的例子大家都很熟悉,那就是单例。使用Swift...
    TomatosX阅读 1,018评论 0 1
  • 尽管在我之前的博文里我就写过关于管理状态的那些坑,但是有时候我们就是无法避免它们。其中一类管理状态的方式我们耳熟能...
    一黑阅读 266评论 0 1
  • 往事回忆之ObjC单例Swift是Objective-C的一种自然演变,它用如下的方式实现单例: 在这个现成方案中...
    王小宾阅读 3,104评论 0 5
  • 问题 最近排查一个crash 问题,读了一下crash Log以后,发现堆栈报的错误信息非常奇怪。相似在对一个单例...
    还是那个西海阅读 9,840评论 0 9