单例模式
定义
单例模式,简单的说就是一个类始终只对应同一个对象,每次获取这个类的对象获得的都是同一个实例
如果一个类始终只能创建一个实例,那么这个类被称为单例类。单例类只有一个全局的接口来访问这个实例。当第一次载入的时候,他通常使用延迟加载的方创建唯一单例。在程序中一个单例只初始化一次,为了保证在使用中始终存在,单例类的存储是在存储器的全局区域,在编译时分配内存,只要程序在运行单例就始终存在,占用内存。在APP结束运行后释放这部分内存。但是在系统方法中,存在未知的自动释放池,如果对一个对象进行自动释放的话,可能进入未知的释放池,出现内存问题。即为单例模式不能自动释放的原因
特点
- 单例类是一个类,这个类创造出的对象是单例对象
- 单例对象使用类方法创建
- 单例一旦被创建出来,直到程序结束运行才会释放
- 单例不用我们来管理内存,内存会随着程序关闭而被释放
使用原因
节省内存,防止一个实例被重复创建从而占用内存空间。
缺点
- 全局状态:可能导致全局状态的存在,使得程序难以调试,一个地方修改容易导致很多地方发生变化
- 难以拓展:单例模式的实例是固定的,难以拓展以支持多个实例,必须修改代码,使单例类丢失单例的特性
- 隐藏依赖关系:可能会隐藏单例类的依赖关系,代码更加耦合。
模式介绍
懒汉模式
是一种常见的单例设计模式,其主要特点是在需要时在创建单例对象的实现,通过延迟对象的初始化来节省资源和提高性能。适用于访问量较小的情况,使用时间来换取空间
同步锁实现
#import "Person.h"
@implementation Person
static id instance = nil;
+ (instancetype)sharedInstance {
@synchronized (self) {
if (!instance) {
instance = [[super allocWithZone:NULL] init];
}
}
return instance;
}
@end
这一段代码提供了一个全局访问点sharedInstace类方法提供给外部访问这个单例,在方法内部通过添加了一个同步锁,限制了同一时间只有一个线程访问临界区的代码。但是这里如果instance已经存在是没有必要进入锁的,所以可以再加个判断如下:
#import "Person.h"
@implementation Person
static id instance = nil;
+ (instancetype)sharedInstance {
if (!instance) {
@synchronized (self) {
if (!instance) {
instance = [[super allocWithZone:NULL] init];
}
}
}
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
if (!instance) {
@synchronized (self) {
if (!instance) {
instance = [super allocWithZone:zone];
}
}
}
return instance;
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return self;
}
@end
对于allocWithZone的参数,传什么其实都无所谓,分配的内存区域不会改变
内层if的作用:假设有两个线程A和B同时调用了sharedInstance,线程A先一步获取锁,进入代码块,此时线程B等待锁释放,当线程A初始化instance后释放锁,B获得锁,如果没有内层if判断,会再次创建实例,破坏了单例的唯一性
dispatch_once
通过一个静态的dispatch_once变量来跟踪代码的执行状态,第一次调用时原子操作发现标记状态为未执行代码块,并原子性的将标记设为已执行,后续监测到标记为已执行,就会跳过该代码块。非常高效、没有阻塞开销
#import "Person.h"
@implementation Person
static id instance = nil;
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[super allocWithZone:NULL] init];
});
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [super allocWithZone:zone];
});
return instance;
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return self;
}
@end
我们打印出不同情况下的对象地址如下:
饿汉模式
在类加载过程就完成这个单例的创建和初始化
实现
#import "Person.h"
@implementation Person
static id instance = nil;
+ (void)load {
instance = [[super allocWithZone:NULL] init];
}
+ (instancetype)sharedInstance {
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return instance;
}
- (instancetype)init {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"init一次");
});
return self;
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return self;
}
@end
总结
懒汉模式
优点
- 延迟加载,节省资源,提高性能。
- 避免在程序启动时就创造不必要的对象,造成额外开销
- 可以通过加锁实现线程安全
缺点
- 为实现线程安全使用锁机制,可能会引起一些性能开销
饿汉模式
优点
- 实现简单,不需要考虑线程安全问题,实例在类加载时就已经创建了,所以不用考虑创建多个实例的风险
缺点
- 程序启动时就加载创建实例,可能会浪费资源,引起性能问题
- 如果单例对象的创建需要建立在某些外部前提下,那么不适合饿汉模式