单例设计模式(Objective-C)【Singleton Design Pattern】

本文将以 Objective-C 语言为媒介,介绍单例模式的原理、其在 iOS 开发当中的应用以及一些编写单例代码的技巧。

Demo

概念

单例:即单独的实例。

单例模式:以单例为核心的设计模式。在它的核心结构中只包含一个特殊类(即单例)。通过单例模式可以保证系统中,应用该模式的类一个类只有一个对象实例。

单例类:即按照单例设计模式来设计的类,整个程序中只会存在一个此类的实例,且提供一个类方法的入口来访问这个实例。是 iOS 开发对于单例模式的主要应用场景。

单例类的生命周期:程序于编译时初始化单例类, 并存于内存(全局区域)中,直到程序退出时由系统自行释放。

在 iOS 系统中,有如下几个典型的单例类:

  • 类对象(在应用程序范围内,每个类的 Class 仅有一个实例)
  • UIApplication
  • NSNotificationCenter
  • NSFileManager
  • NSUserDefaults
  • NSURLCache
  • NSHTTPCookieStorage

单线程单例

1
2
3
4
5
6
7
8
static Singleton_SingleThread *instance;

+ (instancetype)sharedInterface {
if (!instance) {
instance = [[self alloc] init];
}
return instance;
}

最简单直观的单例模式类,在第一次访问单例接口 + (instancetype)sharedInterface 的时候创建一个实例,之后再次访问则返回已创建好的实例。

但在多线程的环境下,同时访问单例接口的时候容易创建多个实例,造成不可控的风险。基于此,单线程单例很少应用于开发当中。

同步锁单例

为了解决单线程单例在多线程环境下明显的缺点,首先会想到加锁,利用 @synchronized,就有了下面这种同步锁单例

1
2
3
4
5
6
7
8
9
10
static Singleton_Lock *instance;

+ (instancetype)sharedInterface {
@synchronized (self) {
if (!instance) {
instance = [[self alloc] init];
}
}
return instance;
}

如此一来,就解决了多线程环境下访问单例接口的问题。但这还不够好,因为多个线程同时访问 @synchronized 中的代码时,只有一个线程会被放行,而其他的都需要等待,造成了一定程度的性能消耗。这样的阻塞是没有必要的,因为只有当 instance 没有被创建时,加锁才有意义,一旦 instance 被创建,锁自然就成了负担。

GCD单例

为了解决 @synchronized 的性能问题,GCD为我们提供了一套简单又粗暴的单例类实现方案,即 dispatch_once

1
2
3
4
5
6
7
8
9
static Singleton_GCD *instance;

+ (instancetype)sharedInterface {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}

原理是这样的:

  1. static 表示 instance 将作为静态变量存入内存当中。
  2. dispatch_once_t 类型其实就是 long 类型(typedef long dispatch_once_t;)。
  3. onceTokendispatch_once 来判断内部代码是否执行过的标志位。
  4. onceToken 为 0 时,线程执行 block 中的代码。
  5. onceToken 为 -1 时,线程跳过 block 中的代码。
  6. onceToken 为其他任何可能的值时,线程被阻塞,等待 onceToken 的值发生变化。
  7. A线程首先调用单例接口,先改变 onceToken 的值为一个任意值,再执行 block 中的代码(此时若其他线程也想执行 block 中的代码,则会因为判定 onceToken 的值而阻塞在外部),最后执行完 block,将 onceToken的值置为 -1,其他线程直接跳过 block。
  8. 这样的模式只会在首次实例时阻塞线程,一旦实例创建完毕(此时 onceToken 已变为 -1),就不会再阻塞线程,而是直接跳过 block。

结语

单例模式是一种非常好用且常见的设计模式,而在 iOS 开发中,使用 GCD 来创建单例类,是目前的最佳方案。

单例的优点很明显,节约系统开支,提升程序运行的效率,且开发者可以凭借单例特有的唯一性来定位 bug。

但仍需注意的是,正因为单例独有的一旦创建就只有一份(地址不会变)的特性,导致我们无法手动销毁这个单例,这块内存只能由系统去释放。这就意味着,单例一旦处于闲置状态,那么它就是一个单纯的资源消耗者。所以,一个程序不应该有太多的单例,否则将会占用大量的内存。