这是我参与8月更文挑战的第17天,活动详情查看: 8月更文挑战
单例在不同的语言中实现方式可能不一定完全相同,但其宗旨应该是一样的:单例类在整个程序运行期间有且仅有一个实例;所以,在开发过程中,根据需求合理使用单例类;
单例的优缺点
优点
- 一个类只被实例化一次,提供了对唯一实例的受控访问
- 节省系统资源
- 允许可变数目的实例
因为其上面的特点,对于项目中的个别场景的传值,存储状态等等更加方便
缺点
- 一个类只有一个对象,可能造成责任过重,在一定程度上违背了
单一职责原则
- 由于单例模式中没有抽象层,因此单例类的扩展有很大困难
- 滥用单例将带来一些负面问题,如:为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失
- 单例实例一旦创建,对象指针是保存在静态区的,那么在堆区分配空间只有在应用程序终止后才会被释放
单例的实现
Objective-C
第一种方式
- 创建单例类
+ (instancetype)shareInstance {
static AppManager *instance = nil ;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (instance == nil) {
instance = [[AppManager alloc] init];
}
});
return instance;
}
复制代码
- 防止alloc,init,new引起的错误
// 防止外部调用alloc 或者 new
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [AppManager shareInstance];
}
复制代码
- 防止NSCopying引起的错误
// 防止外部调用copy
- (id)copyWithZone:(nullable NSZone *)zone {
return [AppManager shareInstance];
}
复制代码
- 防止mutableCopy引起的错误
// 防止外部调用mutableCopy
- (id)mutableCopyWithZone:(nullable NSZone *)zone {
return [AppManager shareInstance];
}
复制代码
第二种方式
创建之后,我们直接禁止外部使用init
,new
等操作;
我们在单例
的.h
文件中添加一下代码:
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
- (id)copy NS_UNAVAILABLE; // 没有遵循协议可以不写
- (id)mutableCopy NS_UNAVAILABLE; // 没有遵循协议可以不写
复制代码
Swift
class AppManager: NSObject {
static let sharedInstance = AppManager()
// 私有化构造方法,防止其他对象使用这个类的默认方法‘()’来进行初始化
private override init() {}
}
复制代码
注意事项
- 单例必须是唯一的,所以它才被称为单例。在一个应用程序的生命周期里,有且只有一个实例存在。单例的存在给我们提供了一个唯一的全局状态。比如
NSNotificaton
,Application
,NSuserDefault
都是单例. - 为了保持一个单例的唯一性,单例的
构造器
必须是私有的。这防止其他对象也能创建出单例类的实例。 - 为了确保单例在应用程序的整个生命周期是唯一的,它就必须是线程安全的。简单来说,如果你写单例的方式是错误的,就有可能会有两个线程尝试在同一时间初始化同一个单例,这样就有潜在的风险得到两个不同额单例。这就意味着我们需要用到
GCD
的dispatch_once
来确保初始化单例的代码在运行时只执行一次。
那么为什么用Swift
创建的单例没有看到dispatch_once
呢?
根据官方文档中的说明:
全局变量
的延迟构造器
(也适用于结构和枚举的静态成员)在第一次
访问全局变量时运行,并作为dispatch_once
启动,以确保初始化是原子的
。这使得在代码中使用dispatch_once
有了一个很酷的方法:只要声明一个带有初始化式的全局变量并将其标记为private
即可。
单例dispatch_once的底层原理
我们经常写单例的实现时,经常会这么写:
+ (instancetype)shareInstance {
static XXXXX *instance = nil ;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (instance == nil) {
instance = [[XXXXX alloc] init];
}
});
return instance;
}
复制代码
那么dispatch_once
是如何实现的呢?我们来看一下:
我们发现dispatch_once
就是通过dispatch_once_f
这个函数来实现单例类的;
val
就是&onceToken
,也就是全局静态变量
;(作为条件控制来使用)block
是要执行的任务;_dispatch_Block_invoke(block)
是对任务的封装;
接下来,我们来看一下dispatch_once_f
的具体实现:
dispatch_once_gate_t
:将val
封装成一个开关,进行强制类型转换为l
;_dispatch_once_gate_tryenter(l)
:我们第一次创建时,将会执行至此;
进入_dispatch_once_gate_tryenter
函数内部:
os_atomic_cmpxchg
进行原子操作
,进行锁的处理,说明其实有线程控制的;_dispatch_lock_value_for_self
当前队列中的线程空间的锁,防止多线程(单例类的线程安全问题);
接下来进入_dispatch_once_callout
函数:
- 函数
_dispatch_client_callout
进行任务ctxt
的执行; _dispatch_once_gate_broadcast
:任务执行完毕之后进行广播;
两个宏定义,会有一个执行;我们针对_dispatch_once_mark_done
分析一下:
将dog
设置dgo_once
为DLOCK_ONCE_DONE
;
然后在dispatch_once_f
的实现中:
第二次进来时,判断dgo_once
为DLOCK_ONCE_DONE
的话直接return
;
如果没有设置DLOCK_ONCE_DONE
,并且_dispatch_once_gate_tryenter
被上锁,那么将会执行_dispatch_once_wait(l)
,进行无限期等待,等待开锁;