这是我参与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),进行无限期等待,等待开锁;