iOS单例设计模式具体解说(单例设计模式不断完好的过程)
在iOS中有非常多的设计模式,有一本书《Elements of Reusable Object-Oriented Software》(中文名字为《设计模式》)讲述了23种软件设计模式。这本书中的设计模式都是面向对象的。非常多语言都有广泛的应用。在苹果的开发中,当然也会存在这些设计模式。我们所使用的不管是开发Mac OX系统的Cocoa框架还是开发iOS系统的Cocoa Touch框架,里面的设计模式也是由这23种设计模式演变而来。
本文着重具体介绍在开发iOS时採用的单例模式。从设计过程的演变和细节的完好进行分析,相信大家可以从中获得重要的思路原理而不是只知道应该这么写单例模式却不知为何这么写,当然,理解透彻后,为了我们的开发效率,我们可以将单例模式的代码封装到一个类中然后定义成宏,适配于ARC和MRC模式,让开发效率大大提高。
这些操作在本文中都会一一讲到。接下来就进入正题。
id moviePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
if (moviePlayer == nil) {
// 调用super的allocWithZone方法来分配内存空间
moviePlayer = [super allocWithZone:zone];
}
return moviePlayer;
}
在这里我们初步使用懒载入来控制保证仅仅有一个单例,可是这种仅仅适合在单一线程中使用的情况,要是涉及到了多线程的话。那么就会出现这种情况,当一个线程走到了if推断时,推断为空,然后进入当中去创建对象,在还没有返回的时候。另外一条线程又到了if推断,推断仍然为空。于是又进入进行对象的创建,所以这种话就保证不了仅仅有一个单例对象。于是,我们对代码进行手动加锁。
id moviePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
// 在这里加一把锁(利用本类为锁)进行多线程问题的解决
@synchronized(self){
if (moviePlayer == nil) {
// 调用super的allocWithZone方法来分配内存空间
moviePlayer = [super allocWithZone:zone];
}
}
return moviePlayer;
}
这种话,就能够解决上述问题,可是,每一次进行alloc的时候都会加锁和推断锁的存在,这一点是能够进行优化的(在java中也有对于这种情况的处理),于是在加锁之前再次进行推断,改动代码例如以下:
id moviePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
// 在这里推断,为了优化资源,防止多次加锁和推断锁
if (moviePlayer == nil) {
// 在这里加一把锁(利用本类为锁)进行多线程问题的解决
@synchronized(self){
if (moviePlayer == nil) {
// 调用super的allocWithZone方法来分配内存空间
moviePlayer = [super allocWithZone:zone];
}
}
}
return moviePlayer;
}
到此,在allocWithZone方法中的代码基本完好,接着,在我们进行开发中,也时常会使用到非常多单例。我们在创建单例的时候都不是使用的alloc和init,而是使用的shared加上变量名这样的创建方式,所以,我们自己写单例的话。也应该向外界暴露这种方法。在.h文件里先声明下方法
+ (instancetype)sharedMoviePlayer;
然后在.m文件里实现。逻辑上和allocWithZone方法是一样的
+ (instancetype)sharedMoviePlayer
{
if (moviePlayer == nil) {
@synchronized(self){
if (moviePlayer == nil) {
// 在这里写self和写本类名是一样的
moviePlayer = [[self alloc]init];
}
}
}
return moviePlayer;
}
这个对外暴露的方法完毕之后。我们还须要注意一点。在使用copy这个语法的时候,是可以创建新的对象的,假设使用copy创建出新的对象的话,那么就不可以保证单例的存在了,所以我们须要重写copyWithZone方法。假设直接在.m文件里敲的话。会发现没有提示,这是没有声明协议的原因。可以在.h文件里声明NSCopying协议,然后重写copyWithZone方法:
- (id)copyWithZone:(NSZone *)zone
{
return moviePlayer;
}
在这里没有像上面两个方法一样实现逻辑是由于:使用copy的前提是必须现有一个对象,然后再使用,所以既然都已经创建了一个对象了。那么全局变量所代表的对象也就是这个单例。那么在copyWithZone方法中直接返回就好了
extern id moviePlayer;
moviePlayer = nil;
这时候在这句代码之前创建的单例就销毁了。再次创建的对象就不是同一个了,这样就无法保证单例的存在
#import "NTMoviePlayer.h"
@implementation NTMoviePlayer
static id moviePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
// 在这里推断。为了优化资源,防止多次加锁和推断锁
if (moviePlayer == nil) {
// 在这里加一把锁(利用本类为锁)进行多线程问题的解决
@synchronized(self){
if (moviePlayer == nil) {
// 调用super的allocWithZone方法来分配内存空间
moviePlayer = [super allocWithZone:zone];
}
}
}
return moviePlayer;
}
+ (instancetype)sharedMoviePlayer
{
if (moviePlayer == nil) {
@synchronized(self){
if (moviePlayer == nil) {
// 在这里写self和写本类名是一样的
moviePlayer = [[self alloc]init];
}
}
}
return moviePlayer;
}
- (id)copyWithZone:(NSZone *)zone
{
return moviePlayer;
}
@end
二、单例模式中饿汉式的实现
+ (void)load
{
musicPlayer = [[self alloc]init];
}
接着我们仍然须要重写allocWithZone方法。由于在load方法中是用alloc来创建对象。分配内存空间的,可是在饿汉式中的逻辑就和在懒汉式中的逻辑有所差别了
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
if (musicPlayer == nil) {
musicPlayer = [super allocWithZone:zone];
}
return musicPlayer;
}
在这里,我们能够发现有简洁了非常多。去掉了多线程的问题的加锁方案,我们来分析下原因。首先,在类被载入的时候会调用且仅调用一次load方法,而load方法里面又调用了alloc方法,所以。第一次调用肯定是创建好了对象,并且这时候不会存在多线程问题。
当我们手动去使用alloc的时候,不管怎样都过不了推断,所以也不会存在多线程的问题了。
接下来须要实现shareMusicPlayer方法和copy方法
+ (instancetype)sharedMusicPlayer
{
return musicPlayer;
}
- (id)copyWithZone:(NSZone *)zone
{
return musicPlayer;
}
代码又变简单,这里连推断都不用加,是由于我们使用shareMusicPlayer方法和copy的时候必定全局变量是有值的,而alloc方法中不直接返回是由于在load方法中调用了它,须要去创建一个对象
#import "NTMusicPlayer.h"
@implementation NTMusicPlayer
static id musicPlayer;
+ (void)load
{
musicPlayer = [[self alloc]init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
if (musicPlayer == nil) {
musicPlayer = [super allocWithZone:zone];
}
return musicPlayer;
}
+ (instancetype)sharedMusicPlayer
{
return musicPlayer;
}
- (id)copyWithZone:(NSZone *)zone
{
return musicPlayer;
}
@end
三、使用GCD取代手动加锁推断处理
再次新建一个类NTPicturePlayer。这里将具体说明适用GCD中的方法来取代我们手动加锁的情况,还是按照惯例,在.h文件里声明shared方法。然后在.m文件里使用static定义一个全局变量,首先。重写alloc方法
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
picturePlayer = [[super alloc]init];
});
return picturePlayer;
}
dispatch_once方法是已经在方法的内部攻克了多线程问题的。所以我们不用再去加锁(開始定义了一个static常量。这句代码不是自己写的,敲dispatch_once有个提示的方法就会自己主动生成),dispatch_once在宏观上面表示内部方法仅仅会运行一次。接着是sharedPicturePlayer方法
+ (instancetype)sharedPicturePlayer
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
picturePlayer = [[self alloc]init];
});
return picturePlayer;
}
最后是copy方法的重写
- (id)copyWithZone:(NSZone *)zone
{
return picturePlayer;
}
这种话,GCD版的单例模式(这里是懒汉模式为例)就做好了,以下是整合代码:
#import "NTPicturePlayer.h"
@implementation NTPicturePlayer
static id picturePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
picturePlayer = [[super alloc]init];
});
return picturePlayer;
}
+ (instancetype)sharedPicturePlayer
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
picturePlayer = [[self alloc]init];
});
return picturePlayer;
}
- (id)copyWithZone:(NSZone *)zone
{
return picturePlayer;
}
@end
能够看出,GCD版本号的单例模式比我们之前手动进行加锁的单例模式要简单非常多,因此在实际开发中GCD版本号的单例模式也是使用最多的
四、非ARC情况的单例模式
这里就列举出来了:release、retain、retainCount、autorelease。以下就分别进行重写并说明(以上述GCD版为例):
- (oneway void)release
{ }
括号里的參数是系统生成的,我们仅仅须要将这种方法重写。然后不在里面写代码就能够了
- (instancetype)retain
{
return picturePlayer;
}
- (NSUInteger)retainCount
{
return 1;
}
4、autorelease方法,对这种方法的处理和retain方法类似,我们仅仅须要将对象本身返回,不须要进行自己主动释放池的操作
- (instancetype)autorelease
{
return picturePlayer;
}
这样一来。在非ARC下的单例模式就写好了。以下是整合代码:
#import "NTPicturePlayer.h"
@implementation NTPicturePlayer
static id picturePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
picturePlayer = [[super alloc]init];
});
return picturePlayer;
}
+ (instancetype)sharedPicturePlayer
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
picturePlayer = [[self alloc]init];
});
return picturePlayer;
}
- (id)copyWithZone:(NSZone *)zone
{
return picturePlayer;
}
- (oneway void)release
{ }
- (instancetype)retain
{
return picturePlayer;
}
- (NSUInteger)retainCount
{
return 1;
}
- (instancetype)autorelease
{
return picturePlayer;
}
@end
五、单例模式的代码有用化(封装便于开发直接使用)
我们也许会有这种思路,将单例类放到project中,然后让须要实现单例的类都继承于这个类。这个想法表面上是不错的,可是深入一点去研究的话。就会发现。这个单例类的全部子类所创建出来的单例都是一样的,这就未免不可行了,造成这个的原因是:在子类创建单例对象。实际上最根本上是调用了父类的alloc方法。而在父类中。仅仅会存在一次创建对象,创建之后则是直接返回了创建好的那个单例。通俗来说,当一个子类创建单例对象的时候。调用到了父类的创建方法,获取到了这个单例对象,但假设第二个子类再创建单例对象,调用到父类的创建方法,这时候进行的操作不再是创建新的对象,而是返回第一个子类创建的对象。
所以,这种利用继承关系来简化的方法是不可取的。
// .h文件代码
#define NTSingletonH(name) + (instancetype)shared##name;
// .m文件代码
#define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[super alloc]init];\
});\
return instance;\
}\
+ (instancetype)shared##name\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[self alloc]init];\
});\
return instance;\
}\
- (id)copyWithZone:(NSZone *)zone\
{\
return instance;\
}
相比之前做了一些细节的优化,首先将全局变量的名字改为了instance,这样对于全部的类都是可以共用的,然后利用了括号和#号的联系来使宏定义变的灵活,我们使用的时候在宏定义的括号里敲出我们的单例对象名字就好了(注意因为name这个属性是直接拼接在了shared后面,所以我们在括号里写单例的名字的时候应该将首字母大写),最后要注意一点细节,对于非常大一段代码,直接放到宏中是不可以识别的。所以这里我们须要使用 \
这个符号,这个符号表示后面的一段是属于宏定义中的,所以我们在每条代码前面都加入上了这个符号。
// .h文件代码
#define NTSingletonH(name) + (instancetype)shared##name; // .m文件代码
#define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[super alloc]init];\
});\
return instance;\
}\
+ (instancetype)shared##name\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[self alloc]init];\
});\
return instance;\
}\
- (id)copyWithZone:(NSZone *)zone\
{\
return instance;\
}\
- (oneway void)release\
{\
}\
- (instancetype)retain\
{\
return instance;\
}\
- (NSUInteger)retainCount\
{\
return 1;\
}\
- (instancetype)autorelease\
{\
return instance;\
}
这样。基本都做好的封装,可是有些人仍然认为带上两个类非常麻烦,可不能够将两个类封装成一个类,答案是当然能够。我们能够通过条件编译来进行处理,这里简要说明下条件编译,条件编译类似于if else的工作,可是原则上是有非常大的不同。if else是在执行时进行处理,而条件编译是在编译时就进行处理。也就是说。使用条件编译,能够去在编译的时候检查环境是MRC还是ARC,然后跳转到对应的代码进行执行.。
这样的想法是好的,可是在宏定义中却不是那么实际。由于在宏定义中#是有特殊的作用的。假设任意乱使用#,就会报错。所以我们还是老老实实在推断中写完两套代码吧,以下给出整个代码(整个代码能够收集,自己做一个类)
// .h文件的代码
#define NTSingletonH(name) + (instancetype)shared##name;
// .m文件里的代码(使用条件编译来差别ARC和MRC)
#if __has_feature(objc_arc) #define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[super alloc]init];\
});\
return instance;\
}\
+ (instancetype)shared##name\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[self alloc]init];\
});\
return instance;\
}\
- (id)copyWithZone:(NSZone *)zone\
{\
return instance;\
} #else #define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[super alloc]init];\
});\
return instance;\
}\
+ (instancetype)shared##name\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[self alloc]init];\
});\
return instance;\
}\
- (id)copyWithZone:(NSZone *)zone\
{\
return instance;\
}\
- (oneway void)release\
{\
}\
- (instancetype)retain\
{\
return instance;\
}\
- (NSUInteger)retainCount\
{\
return 1;\
}\
- (instancetype)autorelease\
{\
return instance;\
} #endif
假设你看到了这里。我想,你对单例模式的掌握应该更深了^-^
最新文章
- Linux 进程间通讯详解二
- windows下搭建scrapywindows 7 (64) + python 3.5 (64)
- 查看Query Plan
- .bat脚本基本命令语法
- IE11如何采用其他低级版本调试网页
- “-bash: svn: command not found”
- 清理c盘垃圾(将一下代码复制到记事本然后把后缀名改为xxx.bat,然后双击,就ok了!!)
- wordpress的使用
- Linux如何查找文件安装路径?
- 建立HttpsConnection
- epoll函数及三种I/O复用函数的对比
- Lamada转化字符类型
- select函数的用法
- EditText之边框颜色
- 手把手教你 LabVIEW 串口仪器控制——VISA 驱动下载安装篇
- 蓝桥杯-加法变乘法(java)
- Pandas 学习笔记
- mysql 问题:连不上
- 获取 ip ( 第三方接口 )
- idea 设置 转自 https://www.cnblogs.com/jajian/p/8136672.html