优点:
1、时间和空间
比较上面两种写法:懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
2、线程安全
(1)从线程安全性上讲,不加同步的懒汉式是线程不安全的
(2)饿汉式是线程安全的,因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。
在最开始的OC学习中,我们学习了一个简单的基本的单例模式的创建:首先定义一个全局变量,类型为我们的类,并给这个全局变量赋值为nil;
当我们获取该类的实例对象的时候,程序会判断该全局变量是不是nil,是就创建该实例并将实例对象赋给全局变量,然后返回该实例对象,否则就直接返回该全局变量。
#import "AModel.h"
static id _instance = nil;
@implementation AModel
+ (id)sharInstance {
if (_instance == nil) {
_instance = [[self alloc] init];
}
return _instance;
}
@end
但是上面的这个单例模式还是存在一些问题,比如假如到了多线程的环境里,多个进程同时访问单例,该单例模式也有可能返回不同的对象
#import "AModel.h"
static id _instance = nil;
@implementation AModel
+ (id)sharInstance {
//定义一个dispatch_once_t类型的名为onceToken的静态全局变量,确保它在运行时只会被初始化一次
static dispatch_once_t onceToken;
//调用dispatch_once函数,该函数用于确保一个代码块只执行一次。
//它接受两个参数:一个指向dispatch_once_t类型变量的指针,以及一个表示需要执行一次的代码块的匿名函数
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
@end
#import "AModel.h"
static id _instance = nil;
@implementation AModel
+ (id)sharInstance {
return [[self alloc] init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
//@synchronized的作用是创建一个互斥锁,保证此时没有其他线程对self对象进行修改,保证代码的安全性
@synchronized (self) {
if (_instance == nil) {
_instance = [super allocWithZone:zone];
}
}
return _instance;
}
@end
使用以上的方法,可以使我们通过调用该方法来初始化实例对象,但是如果我们使用 [[xxx alloc] init] 方法来初始化该对象,就会发现返回的还是不同的对象,这是因为使用 [[xxx alloc] init] 方法实际上alloc的过程是调用了allocWithZone方法,所以用不了我们自己定义的初始化方法,因此想要真正完成单例模式,我们还应该重写allocWithZone方法,使alloc的时候返回唯一实例;还应该重写 copyWithZone:方法,避免实例对象的 copy 操作导致创建新的对象:
#import "AModel.h"
static id _instance = nil;
@implementation AModel
+ (id)sharInstance {
return [[self alloc] init];
}
//将我们原先写在自定义初始化方法中的内容写到allocWithZone中
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
}
//重写 copyWithZone:方法,避免实例对象的 copy 操作导致创建新的对象
-(instancetype)copyWithZone:(NSZone *)zone
{
//由于是对象方法,说明可能存在_instance对象,直接返回即可
return _instance;
}
@end
因为我们在这里已经重写了allocWithZone方法,如果再直接使用[[self alloc] init]就会使程序陷入alloc的死循环,因此这里创建实例对象的时候使用父类的allocWithZone方法
为什么全局变量要使用 static?
① static修饰局部变量
其生命周期与全局变量相同,直到程序结束,只有一份内存空间
作用域不变
② static修饰全局变量
只有一份内存空间
全局变量,在其他文件中,可以通过 extern id _instance来声明,然后直接在其他文件中调用。用 static 修饰后 在其他文件不能通过 extern id _instance 声明后 引用
在 MRC 环境中,我们需要考虑如果创建出来的单例对象,被手动 release 了怎么办?所以我们在设计单例模式的时候,需要考虑这种情况。如下:
//重写 retain 方法,不作计数器加1的操作
-(instancetype)retain
{
return _instance;
}
//重写 release 方法,不做任何操作
-(void)release
{
}
//重写 autorelease 方法,返回自身
-(instancetype)autorelease
{
return _instance;
}
//重写 retainCount 方法,返回1
-(NSUInteger)retainCount
{
return 1;
}
饿汉模式就是当类第一次被创建的时候就去创建实例对象,并保存在_instance中,由于第一次加载就创建,内存从程序开始运行的时候就分配了,不适合移动设备。
在使用饿汉模式之前,我们先说load方法和initilized方法。
①当程序刚开始运行的时候,所有的类都会加载到内存中(不管这个类有没有使用),此时就会调用 load 方法
②如果某种操作想要在程序运行的过程中只执行一次,那么这个操作就可以放到 load 方法中
③基于第二点,我们的饿汉模式的单例对象创建就放在 load 方法中
①当类第一次被使用的时候调用(比如,调用类的方法)。
②如果子类没有重写该方法,那么父类的initialized方法可能会被执行多次。所以饿汉模式不能使用这个方法
饿汉模式在类加载的时候就会创建类的实例,而在iOS中,类的实例是在调用init方法时创建的。因此,在饿汉模式中,如果使用init方法来创建实例,就会导致无法创建实例的情况发生。因此,在iOS中,饿汉模式不能使用initilized方法来创建实例。
static id _instance;
@implementation EHanModel
//当类加载到OC运行环境中(内存)时,就会调用一次(一个类只会加载一次)
+ (void) load {
_instance = [[self alloc] init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
@synchronized (self) {
if (_instance == nil) {
_instance = [super allocWithZone:zone];
}
}
return _instance;
}
+ (id)sharInstance {
return [[self alloc] init];
}
@end
由于单例的h文件和m文件一成不变,所以可以抽成宏定义。抽成宏定义需要注意
#define SoundToolH(name) +(instancetype)shared##name;
//调用宏定义SoundToolH(MusicTool)时,就相当于
+(instancetype)sharedMusicTool;
#define SoundToolM(name) \
static id _instance;\
+(instancetype)shared##name\
{\
dispatch_once_t onceToken = NULL;\
dispatch_once(&onceToken)\
{\
_instance = [self alloc]init];\
}\
}