@WilliamAlex大叔
前言
目前流行的社交APP中都离不开单例的使用,我们来举个例子哈,比如现在流行的"糗事百科""美拍"等APP中,当你选择某一个功能时,它都会跳转到登录界面,然而登录界面都是一样的,所以我们完全可以将这个登录控制器设置成一个单例.这样可以节省内存的开销,优化我们的内存,下面纯属个人整理,如果有错误,希望大家指出来,相互进步.下面我们正式开始介绍单例
单例模式
- 单例模式的作用
- 确保在程序运行的过程中,一个类或者是一个对象只有一个实例,一个内存,并且该实例易被外界访问.
- 单例模式的使用场合
- 在整个应用程序中,共享一份资源,这份资源只需要创建初始化1次,就比如前言中所描述的登录界面.
- 单例模式的实例
- 获取主窗口 : [UIApplication sharedApplication]
- 获取某个目录下的文件资源 : [NSFileManager defaultManager]
- 数据存储中的偏好设置 : [NSUserDefaults standardUserDefaults]
在写代码之前,我们好好整理整理思路
- 学习单例的最好方法是从内存地址入手,因为单例的本质是只会创建一份实例,说明它只有一份内存,我们可以通过内存地址触发,慢慢了解单例的好处以及优势.
- 本章主要介绍两种方式创建单例(使用GCD方式和普通创建单例方式)
- GCD方式 : dispatch_once_t
- 普通方式 : if/else语句, @synchronized(加锁)联用
引入单例
- 我们通过新建一个WGStudent类,在ViewController中创建多个WGStudnt类型的对象,打印出它们的地址
// 不要忘记需要导入头文件哦
- (void)viewDidLoad {
[super viewDidLoad];
// 创建对个对象
WGStudent *student1 = [[WGStudent alloc] init];
WGStudent *student2 = [[WGStudent alloc] init];
WGStudent *student3 = [[WGStudent alloc] init];
WGStudent *student4 = [[WGStudent alloc] init];
WGStudent *student5 = [[WGStudent alloc] init];
// 打印对应的地址
NSLog(@"S1=%p,S2=%p,S3=%p,S4=%p,S5=%p",student1,student2,student3,student4,student5);
}
打印结果
S1=0x7ff4fae07a30
S2=0x7ff4fae0e520
S3=0x7ff4fae04580
S4=0x7ff4fae0e390
S5=0x7ff4fae0e430
- 总结 : 通过上述示例,每次都会alloc一次,导致它们的内存地址不一样,但是我们最初的目的只是创建同一个对象,我们都知道,只要alloc一次,系统就会开辟一个新的存储空间,但是根据我们的要求,完全是没有必要另辟新的存储空间的.所以这时候我们就需要引入单例模式.
单例模式的原理
- 原理 : 根据上面的示例,我们可以很清楚的明白,既然我们想要它多次创建,但是只有一份内存,我们只需要重写alloc方法即可吖,在重写的方法中确保进来的对象只创建一次.不错,思路是正确的,但是我们要弄清楚本质,什么才是最严谨的做法.
- 其实我们这里并不是重写alloc方法,创建对象,调用alloc,其实它的本质是调用了alloc的底层:allocWithZone方法,所以我们实现单例模式重写的是allocWithZone而不是alloc方法.
- 我们只需要保证整个进程中,allocWithZone只会调用1次即可实现单例模式.
- 现在我们的目标是将上面的打印中的地址变成同一个内存地址.
创建单例的格式
给外界提供一个接口 :
说明自己的身份,让别人一看就知道它是一个单例
命名规范:share+类名|default+类名|share|类名|standard + 类名
既然做了,我们就要做到最严谨,不管是外界 alloc、init 还是 copy,mutableCopy 都应当只有一份实例重写allocWithZone,让这个方法生成实例的代码只能运行一次即可。
GCD方式 : dispatch_once_t
步骤 :
- 创建一个WGStudent类
- 在.h文件中声明一个类方法shareInstance,供给外界使用
- 在.m文件中,重写allocWithZone方法,保证它在整个进程中只会执行一次.
- 实现声明的类方法,保证它只会被初始化一次
- 为了严谨起见,我们重写copyWithZone以及MutableCopyWithZone方法,这里需要注意,重写这两个对象方法时,需要遵循<NSCopying,NSMutableCopying>两个协议,这样才能找到方法,当然,当我们重写这两个方法以后我们可以不遵守这两个协议,去掉也可以(中重写完毕两个对象方法以后).
dispatch_once_t实现单例代码
在WGStudent.h文件中
#import <Foundation/Foundation.h>
@interface WGStudent : NSObject
/**
* 声明一个类方法,表明自己是一个单例
*/
+ (instancetype)shareInstance;
@end
注意:声明单例的命名规范
注意示例中提出来的问题,下面有详细的解释,先看明白代码.
在WGStudent.m文件中
#import "WGStudent.h"
// 协议可以不遵守吗? (我没有删掉是因为便于理解代码)
@interface WGStudent() <NSCopying, NSMutableCopying>
@end
// onceToken的主要作用是什么?
@implementation WGStudent
// 为什么要定义一个static全局变量?
static WGStudent *_instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
// 这里使用dispatch_once_t的目的是什么?
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
}
+ (instancetype)shareInstance
{
// 这里使用dispatch_once_t的目的是什么?
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
// 重写下面两个对象方法的注意点是什么
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return _instance;
}
@end
打印结果
S1=0x7faceb53b2c0
S2=0x7faceb53b2c0
S3=0x7faceb53b2c0
S4=0x7faceb53b2c0
S5=0x7faceb53b2c0
- 解释示例中提出来的问题 :
- 协议可以不遵守吗? 答案是可以的,我们遵守协议的目的主要是重写copyWithZone和mutableCopyWithZone方法(不然打不出方法来),当我们重写完毕之后,就可以不遵守了.
- onceToken的作用是什么? onceToken的主要作用是用来记录当前的block是否已经执行过了,如果执行过了,那么就不要再次执行.
- 为什么要定义一个static修饰的全局变量? 使用static修饰全局变量主要是保证只有该文件可以使用,外界是没有办法使用的,防止外界将指针清空(注意: static WGStudent *_instance;是一个被强指针指向的全局变量,既然是单例,就要保证在整个进程中单例对象不要释放,也就是说,单例之所以一直存在,是因为有一个强指针指着),如果指针被清空,下面返回的值就会为nil,没有值,还谈什么单例.
- 在allocWithZone方法中使用dispatch_once主要是保证,对象只会被创建一次,只分配一次内存.
- 在shareInstance方法中使用dispatch_once,主要是保证只会初始化一次,比如说:初始化成员属性.为了严谨起见,在类方法中不能直接返回,因为它可能第一次创建,为空返回值就会返回nil.
- 重写两个对象方法的注意点是什么?前面我们已经说过了,也是为了严谨起见,如果外界使用copy或者mutableCopy创建对象,那我们也将它弄成单例.但是如果你直接敲copy是没有这两个对象方法的,我们必须要遵守<NSCopying,NSMutableCopying>两个协议才能敲出方法,当我们重写完毕时,你可以将协议删掉.
普通方式if来创建单例
首先我们来写一份不够严谨的代码,看看问题出来哪里
#import "WGStudent.h"
@interface WGStudent() <NSCopying, NSMutableCopying>
@end
@implementation WGStudent
// 定义全局变量,保证整个进程运行过程中都不会释放
static WGStudent *_instance;
// 保证整个进程运行过程中,只会分配一个内存空间
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
if (nil == _instance) {
_instance = [super allocWithZone:zone];
}
return _instance;
}
+ (instancetype)shareInstance
{
if (nil == _instance) {
_instance = [[self alloc] init];
}
return _instance;
}
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return _instance;
}
@end
打印结果
S1=0x7febf153ba80
S2=0x7febf153ba80
S3=0x7febf153ba80
S4=0x7febf153ba80
S5=0x7febf153ba80
- 注意 : 看到上面的打印结果,咦o,内存地址是一样的,可以了呀,为什么还说不够严谨呢.你丫装逼失败了吧!!!
- 细心的朋友已经看出来是怎么回事了,用if是不够安全的,我们忽略了多线程这点.
- 我们来分析一下哈,假如现在有多条线程,假设线程1进入allocWithZone方法中了,判断了一下,咦! 没有值,线程1进来了,有可能线程1还没有赋值,没有分配存储空间,线程2也进入allocWithZone方法了,判断一下,好家伙! 也没有值,这时候线程1已经赋值完毕,分配好了内存空间,线程2也开始了赋值,分配新的内存空间,这就造成了多次分配内存空间,这和单例模式的本质原理是相违背的.
- 解决办法也很简单,给线程加锁.
解决后的代码
#import "WGStudent.h"
@interface WGStudent()
@end
@implementation WGStudent
// 定义全局变量,保证整个进程运行过程中都不会释放
static WGStudent *_instance;
// 保证整个进程运行过程中,只会分配一个内存空间
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (nil == _instance) {
_instance = [super allocWithZone:zone];
}
return _instance;
}
}
+ (instancetype)shareInstance
{
@synchronized(self) {
if (nil == _instance) {
_instance = [[self alloc] init];
}
return _instance;
}
}
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return _instance;
}
@end
打印结果
S1=0x7fd39af539d0
S2=0x7fd39af539d0
S3=0x7fd39af539d0
S4=0x7fd39af539d0
S5=0x7fd39af539d0
- 注意 : 使用线程加锁一定要注意它的位置. 线程加锁的锁对象一般是当前类(self)原因是当前类也是只有一个内存,唯一的.
以上就是实现在ARC环境下创建单例的两种方法
接下来我们来创建MRC环境下的单例
- 我们来分析一下哈,ARC与MRC的主要区别是什么(具体的区别后续我会更新的),主要区别就是是否需要手动管理内存.
下面是MRC环境下的代码
在.h文件中声明单例方法
#import <Foundation/Foundation.h>
@interface WGStudent : NSObject
/**
* 声明一个类方法,表明自己是一个单例
*/
+ (instancetype)shareInstance;
@end
在.m文件中重写方法
#import "WGStudent.h"
@interface WGStudent()
@end
@implementation WGStudent
#pragma mark - ARC环境下的单例
// 定义全局变量,保证整个进程运行过程中都不会释放
static WGStudent *_instance;
// 保证整个进程运行过程中,只会分配一个内存空间
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (nil == _instance) {
_instance = [super allocWithZone:zone];
}
return _instance;
}
}
+ (instancetype)shareInstance
{
@synchronized(self) {
if (nil == _instance) {
_instance = [[self alloc] init];
}
return _instance;
}
}
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return _instance;
}
#pragma mark - MRC环境下的单例(还要加上上面的方法)
#if __has_feature(objc_arc)
// ARC :就执行上面重写的方法即可
#else
// MRC : 除了执行上面的方法,还需要重写下面的方法.
- (oneway void)release {
// 什么都不用做,安静的看着其他方法装逼即可
}
- (instancetype)retain
{
return _instance;
}
- (NSUInteger)retainCount
{
return MAXFLOAT;
}
#endif
@end
打印结果
S1=0x7f9b6bd90fb0
S2=0x7f9b6bd90fb0
S3=0x7f9b6bd90fb0
S4=0x7f9b6bd90fb0
S5=0x7f9b6bd90fb0
- 注意点 :
- 需要将ARC环境设置为MRC环境
- 示例中我讲ARC和MRC都混合在了一起,需要记住判断当前环境是ARC还是MRC的宏
- (void)currentEnvironment
{
#if __has_feature(objc_arc)
// ARC
NSLog(@"ARC环境");
#else
// MRC
NSLog(@"MRC环境");
#endif
}
以上就是ARC和MRC环境下的单例
- 在实际开发中,我们为了提高工作效率,一般不会每次需要使用单例时,都老实巴交一步一步的编写单例,我习惯将他们抽取出来,定义成一个宏,到时候使用单例时,直接调用宏,我们只需要传入一个参数.
单例宏代码
// 直接将单例的实现(ARC和MRC)全部定义到PCH文件中,,设置PCH文件路径即可
#define SingleH(instance) +(instancetype)share##instance;
#if __has_feature(objc_arc)
//ARC
#define SingleM(instance) static id _instance;\
\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
+(instancetype)share##instance\
{\
return [[self alloc]init];\
}\
\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}
#else
//MRC
#define SingleM(instance) static id _instance;\
\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
+(instancetype)share##instance\
{\
return [[self alloc]init];\
}\
\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(oneway void)release\
{\
}\
-(instancetype)retain\
{\
return _instance;\
}\
\
-(NSUInteger)retainCount\
{\
return MAXFLOAT;\
}
#endif
- 注意点 :
- 每一行都需要''不然下一行不能识别
- 不要在注释后面添加''.否则后面的全部都会变成注释
- 在实际开发中,我们可以定义的方法不一样,我们可以使用"##"两个井号让方法变成可变的参数,我们传入什么,它就是什么.
- 注意定义全局变量的时候,我们定义的类是不一样的,所以我们需要将它改为id类型.
这里需要重点听 : 有的初学者朋友可能会使用继承,这样就不用把它定义成宏了,我上面就说过了,我们千万不能在单例中使用继承,原因我们看代码,不要耍流氓
使用继承
- 使用继承,首先创建一个父类,WGSignaltonTool,在父类的.h文件中声明单例方法,在.m文件中实现单例方法
在.h文件中
#import <Foundation/Foundation.h>
@interface WGSignaltonTool : NSObject
/**
* 声明一个类方法,表明自己是一个单例
*/
+ (instancetype)shareInstance;
@end
在.m文件中
#import "WGSignaltonTool.h"
@implementation WGSignaltonTool
#pragma mark - ARC环境下的单例
// 定义全局变量,保证整个进程运行过程中都不会释放
static WGSignaltonTool *_instance;
// 保证整个进程运行过程中,只会分配一个内存空间
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (nil == _instance) {
_instance = [super allocWithZone:zone];
}
return _instance;
}
}
+ (instancetype)shareInstance
{
@synchronized(self) {
if (nil == _instance) {
_instance = [[self alloc] init];
}
return _instance;
}
}
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return _instance;
}
#pragma mark - MRC环境下的单例(还要加上上面的方法)
#if __has_feature(objc_arc)
// ARC :就执行上面重写的方法即可
#else
// MRC : 除了执行上面的方法,还需要重写下面的方法.
- (oneway void)release {
// 什么都不用做,安静的看着其他方法装逼即可
}
- (instancetype)retain
{
return _instance;
}
- (NSUInteger)retainCount
{
return MAXFLOAT;
}
#endif
@end
创建两个子类:WGPerson和WGStudent,分别继承WGSignaltonTool,两个子类只需要继承父类即可,什么都不用写
继承完毕父类,我们来到ViewController.m文件,导入两个子类,然后在ViewDidLoad中打印它们的内存地址.
只打印WGPerson类的地址(单独打印WGStudent类的地址情况和WGPerson类类似,所以,这里就打印一个啦)
NSLog(@"%@,%@",[WGPerson shareInstance],[[WGPerson alloc] init]);
打印结果
<WGPerson: 0x7f9912d93b40>
<WGPerson: 0x7f9912d93b40>
- 结论 : 感觉使用继承也是可以的吖,打印出来的地址是一样的,我们先别着急,我们接着来看两个一起打印是是什么结果.
NSLog(@"%@,%@",[WGStudent shareInstance],[[WGStudent alloc] init]);
NSLog(@"%@,%@",[WGPerson shareInstance],[[WGPerson alloc] init]);
打印结果
单例[1569:88929] <WGStudent: 0x7f9daa4032f0>,<WGStudent: 0x7f9daa4032f0>
单例[1569:88929] <WGStudent: 0x7f9daa4032f0>,<WGStudent: 0x7f9daa4032f0>
- 总结 : 你看哈,当我们两个子类一起打印的时候们就会发现,打印出来的结果全是WGStudent,地址也是一样的.这明显是不对的,所以我们千万不能使用继承.