本文将以 Objective-C 语言为媒介,介绍单例模式的原理、其在 iOS 开发当中的应用以及一些编写单例代码的技巧。
概念
单例:即单独的实例。
单例模式:以单例为核心的设计模式。在它的核心结构中只包含一个特殊类(即单例)。通过单例模式可以保证系统中,应用该模式的类一个类只有一个对象实例。
单例类:即按照单例设计模式来设计的类,整个程序中只会存在一个此类的实例,且提供一个类方法的入口来访问这个实例。是 iOS 开发对于单例模式的主要应用场景。
单例类的生命周期:程序于编译时初始化单例类, 并存于内存(全局区域)中,直到程序退出时由系统自行释放。
在 iOS 系统中,有如下几个典型的单例类:
- 类对象(在应用程序范围内,每个类的 Class 仅有一个实例)
- UIApplication
- NSNotificationCenter
- NSFileManager
- NSUserDefaults
- NSURLCache
- NSHTTPCookieStorage
单线程单例
1 | static Singleton_SingleThread *instance; |
最简单直观的单例模式类,在第一次访问单例接口 + (instancetype)sharedInterface
的时候创建一个实例,之后再次访问则返回已创建好的实例。
但在多线程的环境下,同时访问单例接口的时候容易创建多个实例,造成不可控的风险。基于此,单线程单例很少应用于开发当中。
同步锁单例
为了解决单线程单例在多线程环境下明显的缺点,首先会想到加锁,利用 @synchronized
,就有了下面这种同步锁单例。
1 | static Singleton_Lock *instance; |
如此一来,就解决了多线程环境下访问单例接口的问题。但这还不够好,因为多个线程同时访问 @synchronized
中的代码时,只有一个线程会被放行,而其他的都需要等待,造成了一定程度的性能消耗。这样的阻塞是没有必要的,因为只有当 instance
没有被创建时,加锁才有意义,一旦 instance
被创建,锁自然就成了负担。
GCD单例
为了解决 @synchronized
的性能问题,GCD为我们提供了一套简单又粗暴的单例类实现方案,即 dispatch_once
。
1 | static Singleton_GCD *instance; |
原理是这样的:
static
表示instance
将作为静态变量存入内存当中。dispatch_once_t
类型其实就是long
类型(typedef long dispatch_once_t;
)。onceToken
是dispatch_once
来判断内部代码是否执行过的标志位。- 当
onceToken
为 0 时,线程执行 block 中的代码。 - 当
onceToken
为 -1 时,线程跳过 block 中的代码。 - 当
onceToken
为其他任何可能的值时,线程被阻塞,等待onceToken
的值发生变化。 - A线程首先调用单例接口,先改变
onceToken
的值为一个任意值,再执行 block 中的代码(此时若其他线程也想执行 block 中的代码,则会因为判定onceToken
的值而阻塞在外部),最后执行完 block,将onceToken
的值置为 -1,其他线程直接跳过 block。 - 这样的模式只会在首次实例时阻塞线程,一旦实例创建完毕(此时
onceToken
已变为 -1),就不会再阻塞线程,而是直接跳过 block。
结语
单例模式是一种非常好用且常见的设计模式,而在 iOS 开发中,使用 GCD 来创建单例类,是目前的最佳方案。
单例的优点很明显,节约系统开支,提升程序运行的效率,且开发者可以凭借单例特有的唯一性来定位 bug。
但仍需注意的是,正因为单例独有的一旦创建就只有一份(地址不会变)的特性,导致我们无法手动销毁这个单例,这块内存只能由系统去释放。这就意味着,单例一旦处于闲置状态,那么它就是一个单纯的资源消耗者。所以,一个程序不应该有太多的单例,否则将会占用大量的内存。