单例方法

尽管在我之前的博文里我就写过关于管理状态的那些坑,但是有时候我们就是无法避免它们。其中一类管理状态的方式我们耳熟能详 - 单例。但是在Swift中有好几种不同的方式来实现一个单例。到底哪一个才是正确的方式呢?在这边博客里,我将和你好好聊聊单例的历史和在Swift中单 例正确的实现方式。

如果你想直接就看在Swift中如何正确地写出单例同时看到证明其“正确性”,你可以直接滚动到这篇博文的底部。

让我们先回忆一下
Swfit源于Objective-C,高于Objective-C。在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];
    });
    return sharedInstance;
}
 @end

当我们把它写出来之后,就能清楚的看到一个单例的架构,接下来我们把单例的规则理一理,以便更好的理解它:

单例的道义
关于单例,有三件事是你必须要记住的:

单例必须是唯一的,所以它才被称为单例。在一个应用程序的生命周期里,有且只有一个实例存在。单例的存在给我们提供了一个唯一的全局状态。比如我们熟悉的NSNotification,UIApplication和NSUserDefaults都是单例。
为了保持一个单例的唯一性,单例的构造器必须是私有的。这防止其他对象也能创建出单例类的实例。感谢所有帮我指出这点的人。
为了确保单例在应用程序的整个生命周期是唯一的,它就必须是线程安全的。当你一想到并发肯定一阵恶心,简单来说,如果你写单例的方式是错误的,就 有可能会有两个线程尝试在同一时间初始化同一个单例,这样你就有潜在的风险得到两个不同的单例。这就意味着我们需要用GCD的dispatch_once 来确保初始化单例的代码在运行时只执行一次。
成为独一无二并只在一个地方做初始化并不难。这篇博客接下来要记住的内容就是要单例遵守了更难察觉的dispatch_once的规则。

Swift单例
打从Swift 1.0开始,就有好几种创建单例的方法。在这里,这里和这里都有详细的说明。但谁有空点进去看呢?先来个剧透;一共有四种写法,请让我一一道来:

最丑陋的写法

class TheOneAndOnlyKraken {
    class var sharedInstance: TheOneAndOnlyKraken {
        struct Static {
            static var onceToken: dispatch_once_t = 0
            static var instance: TheOneAndOnlyKraken? = nil
        }
        dispatch_once(&Static.onceToken) {
            static.instance = TheOneAndOnlyKraken()
        }
        return Static.instance!
    }
}

这种写法其实就是把Objective-C中的写法照搬过来。我觉得奇丑无比因为Swift是一门更加简洁且表达力更强的语言。我们要做的比那些搬运工要好,要比他们好。

结构体写法

class TheOneAndOnlyKraken {
    class var sharedInstance: TheOneAndOnlyKraken {
        struct Static {
            static let instance = TheOneAndOnlyKraken()
        }
        return Static.instance
    }
}

这个在Swift 1.0的时候必须得这么写,因为那个时候,类还不支持全局类变量。而结构体却支持。正因为在全局变量上的局限性,我们不得不这么写。这比直接把 Objective-C那一套搬过来要好,但是还不够。好玩的是,在Swift 1.2发布后的几个月后,我还是经常能看到这些的写法,但这个以后再表。

全局变量写法(又名“一句话写法”)

private let sharedKraken = TheOneAndOnlyKraken()
class TheOneAndOnlyKraken {
    class var sharedInstance: TheOneAndOnlyKraken {
        return sharedInstance
    }
}

在Swift 1.2之后,我们拥有了访问控制修饰词和可以使用全局类变量。这意味着我们不用整一个全局变量集群,也可以防止命名空间冲突。这才是我心目中Swift应该有的样子。

这会你可能会问我为什么没有在我们的结构体和全局变量实现中看不到dispatch_once。但其实Apple指出,这两个方法同时满足了我上面提到的dispatch_once条文。下面就截取他们写的Swift Blog中的一段话来证明他们以将dispatch_once整合进去了:

“全局变量(静态成员变量和结构体以及枚举)的延迟构造器在其被第一次访问时会加载,并以dispatch_once的方式启动来确保初始化的原子性。这让你写代码时可以用一种很酷的方式来使用dispatch_once:直接用一个全局变量的构造器去做初始化并用private来修饰。“ — Apple’s Swift Blog

就官方文献来看,Apple就给出这点说明。但这仅意味着对全局变量和结构体/枚举的静态成员我们是有证据的!目前来看,100%不会错的是用一个 全局变量将一个单例的初始化包在一个隐含了dispatch_once的延迟构造器中。但是我们的全局类变量怎么办呢?!?!?!?

正确的写法

class TheOneAndOnlyKraken {
    static let sharedInstance = TheOneAndOnlyKraken()
}

为了写这篇文章,我做了一番研究。事实上,之所以写这篇文章,是因为今日在Capital One的一次讨论,一位PR希望在我们所有的应用中用统一的方式来写单例。我们其实知道什么才是单例“正确”的书写方式,但是我们无法自证。如果不旁征博 引来证明我们是正确的简直就是徒劳。这是我和缺乏信息的互联网/博文圈之间的较量。每个人都知道如果网上没写那就不是真的。这让我非常难过。

我的搜索达到了互联网的尽头(Google到了第10页)却啥也没得到。难道真就发表一行单例的证明吗?也许有人做了,就是没找到而已。

所以我觉得我自己把所有的初始化构造器的方法都实现一遍,然后通过断点来检查他们。当分析完每个栈帧我所发现的相似点的时候,我终于发现期待已久的东西 - 证据!

直接上图,对了(还有emoji类哦!):

使用全局单例

使用一行单例

第一张图显示了一个全局let实例化。红框表示的地方就是证据。在实际去实例化Karken单例之前,是先由一个swfit_once调用了一个swift_once_block_invoke。加上Apple说他们通过一个dispatch_once的block去延迟初始化一个全局变量,我们现在可以说这就证明了他们所说的。

带着这个信息,我又跟踪了我们既亮眼又简洁的一行单例。正如第二张图所以,两者简直一样!这就足以证明我们的一行单例实现是正确的。现在全世界都清净了。还有,既然这篇文章已经上了互联网,那么它肯定就是真理!

不要忘了INIT的私有化!
@davedelong,Apple 的架构师,非常含蓄的给我指出,你必须确保你的inits是私有的。只有这样才能确保你的单例是真正的独一无二,也能防止其他对象通过访问控制机制来创建 他们自己的但是是你这个类的单例。因为在Swift中,所有对象的构造器默认都是public,你需要重写你的init让其成为私有的。这并不难实现而且 也能确保我们的一行单例的优雅和简洁:

class TheOneAndOnlyKraken {
    static let sharedInstance = TheOneAndOnlyKraken()
    private init() {} // 这就阻止其他对象使用这个类的默认的'()'初始化方法
}

这么做能确保任何类如果尝试通过()初始化方法来初始化TheOneAndOnlyKraken时,编译器都会报错:

你看!这就是完美的,一行实现单例。

结论
呼应一下jtbandes在top rated answer to swift singletons on Stack Overflow上 精彩的评论,我就无法找到关于使用’let’就能确保线程安全的任何文献。我其实依稀记得在去年的WWDC上有类似的这么一个说法,但你可不能指望读者或 者googlers在尝试证明这就是在Swift中写单例的正确方法的时候就恰巧碰到那说法。不管怎么讲,我希望这篇博文能帮助一些人理解一行单例在 Swift中是打开单例的正确方式。

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

推荐阅读更多精彩内容

  • 在之前的帖子里聊过状态管理有多痛苦,有时这是不可避免的。一个状态管理的例子大家都很熟悉,那就是单例。使用Swift...
    Tank丶Farmer阅读 6,365评论 0 5
  • 往事回忆之ObjC单例Swift是Objective-C的一种自然演变,它用如下的方式实现单例: 在这个现成方案中...
    王小宾阅读 3,150评论 0 5
  • 在使用swift编程语言进行iOS应用开发的时候,我们常常借助单例来进行状态管理,但由于实现单例的方法很多,问题就...
    突然自我阅读 615评论 0 0
  • 问题 最近排查一个crash 问题,读了一下crash Log以后,发现堆栈报的错误信息非常奇怪。相似在对一个单例...
    还是那个西海阅读 10,037评论 0 9
  • 2017年1月1日,元旦佳节,一元之始,万物复苏。新的一年里,我们每一个人的故事都将更加精彩,在元旦佳节以伴郎的身...
    三页薄纸阅读 561评论 0 3