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

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

往事回忆之ObjC单例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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

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

单例规则

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

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心)

1
2
3
4
5
6
7
8
9
10
11
12
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本该更简洁、更有描述力。不要做个搬运工,要做就做的更好。

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

1
2
3
4
5
6
7
8
class TheOneAndOnlyKraken {
    class var sharedInstance: TheOneAndOnlyKraken {
        struct Static {
            static let instance = TheOneAndOnlyKraken()
        }
        return Static.instance
    }
}

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

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

1
2
3
4
5
6
private let sharedKraken = TheOneAndOnlyKraken()
class TheOneAndOnlyKraken {
    class var sharedInstance: 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%安全的。但是对于静态类变量来说,情况又如何?

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

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

1
2
3
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方法,并设置其为私有的。这很简单,而且不会破坏到我们优雅的单行单例方法。

1
2
3
4
class TheOneAndOnlyKraken {
    static let sharedInstance = TheOneAndOnlyKraken()
    private init() {} //This prevents others from using the default '()' initializer for this class.
}

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

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

结论

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

Swift中编写单例的正确方式的更多相关文章

  1. Swift中的单例的实现方式

    单例在iOS日常开发中是一个很常用的模式.对于希望在 app 的生命周期中只应该存在一个的对象,保证对象的唯一性的时候,一般都会使用单例来实现功能.在OC单例的写法如下: @implementatio ...

  2. 在Swift中实现单例方法

    在写Swift的单例方法之前可以温习一下Objective-C中单例的写法: + (instancetype)sharedSingleton{ static id instance; static d ...

  3. 28.怎样在Swift中实现单例?

    1.回忆一下OC中的单例实现 //AFNetworkReachabilityManager中的单例,省略了其他代码 @interface AFNetworkReachabilityManager : ...

  4. iOS中编写单例类的心得

    单例 1.认识过的单例类有哪些: NSUserDefaults.NSNotificationCenter.NSFileManager.UIApplication 2.单例类 单例类某个类在代码编写时使 ...

  5. 在 Swift 中实现单例方法

    我们通常在进行开发的时候,会用到一个叫做 单例模式 的东西.相信大家也都对这种模式非常熟悉了.而且单例的使用在平时的开发中也非常频繁. 比如我们常用到的 NSUserDefaults.standard ...

  6. 【Spring】8、Spring框架中的单例Beans是线程安全的么

    看到这样一个问题:spring框架中的单例Beans是线程安全的么? Spring框架并没有对单例bean进行任何多线程的封装处理.关于单例bean的线程安全和并发问题需要开发者自行去搞定.但实际上, ...

  7. Spring5源码解析-Spring框架中的单例和原型bean

    Spring5源码解析-Spring框架中的单例和原型bean 最近一直有问我单例和原型bean的一些原理性问题,这里就开一篇来说说的 通过Spring中的依赖注入极大方便了我们的开发.在xml通过& ...

  8. iOS开发——多线程OC篇&多线程中的单例

    多线程中的单例 #import "DemoObj.h" @implementation DemoObj static DemoObj *instance; // 在iOS中,所有对 ...

  9. 5.2:缓存中获取单例bean

    5.2  缓存中获取单例bean 介绍过FactoryBean的用法后,我们就可以了解bean加载的过程了.前面已经提到过,单例在Spring的同一个容器内只会被创建一次,后续再获取bean直接从单例 ...

随机推荐

  1. how to remove MouseListener / ActionListener on a JTextField

    I have the following code adding an ActionListener to a JTextField: chatInput.addMouseListener(new j ...

  2. Qt5 基于TCP传输的发送/接收文件服务器(支持多客户端)

    一.实现功能 1.服务器端选择待发送的文件,可以是多个 2.开启服务器,支持多客户端接入,能够实时显示每个客户端接入状态 3.等待所有客户端都处于已连接状态时,依次发送文件集给每个客户端,显示每个客户 ...

  3. 重新mysql-server

    apt-get purge mysql-server │apt-get purge mysql-common │rm -rf /var/log/mysql │rm -rf /var/log/mysql ...

  4. 【Java接口实现动态加载不同的类】

    public interface Person {       public double calcuMonthlySalary(double sal, int type);    }   publi ...

  5. Framework7功能齐全的 iOS7 App 前端框架

    Framework7 是一个功能很全的 HTML 框架,用来构建 iOS7 应用程序. Framework7 允许您灵活搭建列表视图(表视图) .你可以让他们作为导航菜单,你可以在列表里面使用图标,输 ...

  6. vs2010安装svn插件

    vs2010安装svn插件及简单使用 1.下载安装程序,安装 2.配置vs2010 3.check out工程 1.下载安装程序,安装 2.配置vs2010 3.check out工程 Open th ...

  7. C#获取变量名的扩展方法

    // </summary> /// <param name="var_name"></param> /// <param name=&qu ...

  8. 前端自动化学习笔记(一)——Yeoman,bower,Grunt的安装

    最近看视频学习了前端自动化的一些知识,确实让我大开眼界.感觉前端越来越神器了.同时跟着视频自己也尝试运用了一些工具去构建前端项目,但是中间遇见了很多坑,磕磕绊绊的才实现了一点功能,所以打算记录一下学习 ...

  9. (原)css 响应式媒体查询 模板

    @media only screen and (max-width:340px) { html,input{ font-size:80%; } } @media only screen and (ma ...

  10. 缓存 Array.length 是老生常谈的小优化

    问题 缓存 Array.length 是老生常谈的小优化. // 不缓存 for (var i = 0; i < arr.length; i++) { ... } // 缓存 var len = ...