在iOS中有非常多的设计模式,有一本书《Elements of Reusable Object-Oriented Software》(中文名字为《设计模式》)讲述了23种软件设计模式。这本书中的设计模式都是面向对象的。非常多语言都有广泛的应用。在苹果的开发中,当然也会存在这些设计模式。我们所使用的不管是开发Mac OX系统的Cocoa框架还是开发iOS系统的Cocoa Touch框架,里面的设计模式也是由这23种设计模式演变而来。
本文着重具体介绍在开发iOS时採用的单例模式。从设计过程的演变和细节的完好进行分析,相信大家可以从中获得重要的思路原理而不是只知道应该这么写单例模式却不知为何这么写,当然,理解透彻后,为了我们的开发效率,我们可以将单例模式的代码封装到一个类中然后定义成宏,适配于ARC和MRC模式,让开发效率大大提高。
这些操作在本文中都会一一讲到。接下来就进入正题。
在讲述之前,先说明本文的层次结构,本文分成了5个部分,以下依次罗列
1、单例模式中懒汉式的实现
2、单例模式中饿汉式的实现
3、使用GCD取代手动加锁推断处理
4、非ARC情况的单例模式
5、单例模式的代码有用化(封装便于开发直接使用)
前言:
所谓的单例模式,就是要实如今一个应用程序中,相应的一个类仅仅会有一个实例,不管创建多少次,都是同一个对象。大家在开发过程中也见过不少的单例,比方UIApplication、UIAccelerometer(重力加速)、NSUserDefaults、NSNotificationCenter。当然。这些是开发Cocoa Touch框架中的,在Cocoa框架中还有NSFileManager、NSBundle等。在iOS中,懒载入差点儿是无处不在的,事实上,懒载入在某种意义上也是採用了单例模式的思想(假设对象存在就直接返回,对象不存在就创建对象),那么本文就从大家熟悉的懒载入入手进行解说(整个过程都用实际的代码进行说明)。
一、单例模式中懒汉式的实现
新建一个project(本文是single viewproject)。创建一个继承于NSObject的类。命名为NTMoviePlayer,首先我们尝试下使用懒载入。在viewController里面导入NTMoviePlayer.h。定义一个NTMoviePlayer的对象,然后写出懒载入代码。这样好像真的是能够做到在viewController里面仅仅有一个NTMoviePlayer对象,可是假设又创建一个类。然后进行相同的操作,两次创建的对象还会是一样的吗?答案非常明显,不一样,我们能够从这个现象去推导问题发生的根本原因,那就是在不同的类中,创建NTMoviePlayer对象的时候都会进行一个alloc操作,那么这个alloc实际上就是分配内存空间的一个操作,分配了不同的内存区域。那么当然创建了不同的对象,所以,假设要保证应用中就仅仅有一个对象,就应该让NTMoviePlayer类的alloc方法仅仅会进行一次内存空间的分配。这样,找到了问题所在。就去实现代码。重写alloc方法,这里提供了两种方法。一种是alloc,一种是allocWithZone方法,事实上在alloc调用的底层也是allocWithZone方法,所以在此,我们须要重写allocWithZone方法:
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方法中直接返回就好了
到这里。主要的代码差点儿相同都写好了,还须要处理一些细节。首先,我们所声明的全局变量是没有使用static来修饰的,大家在开发过程中所遇见到的全局变量非常多都是使用了static来修饰的。这里进行一个小插曲,简要说明下static的使用,有两种使用方法
1、static修饰局部变量:
简要来说。假设修饰了局部变量的话,那么这个局部变量的生命周期就和不加static的全局变量一样了(也就是仅仅有一块内存区域,不管这种方法运行多少次。都不会进行内存的分配),不同的在于作用域仍然没有改变
2、static修饰全局变量(这点是我们应该注意的):
假设不适用static的全局变量,我们能够在其它的类中使用externkeyword直接获取到这个对象。可想而知,在我们所做的单例模式中。假设在其它类中利用extern拿到了这个对象。进行一个对象销毁。比如:
extern id moviePlayer;
moviePlayer = nil;
这时候在这句代码之前创建的单例就销毁了。再次创建的对象就不是同一个了,这样就无法保证单例的存在
所以对于全局变量的定义,须要加上static修饰符,到此,懒汉式的单例模式就写好了(非ARC和GCD模式在后面讨论),以下给出整合代码
#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
二、单例模式中饿汉式的实现
在第一个模块中。我们进行了单例模式中懒汉式的具体说明,也从懒汉式的模式中知道了实现单例模式的思路。但懒汉式和饿汉式还是有非常大的差别。只是是从实现原理,和代码操作上。在这里先介绍懒汉式和饿汉式和特点(事实上这两个名字都是非常形象的)
1、懒汉式:实现原理和懒载入事实上非常像,假设在程序中不使用这个对象,那么就不会创建。仅仅有在你使用代码创建这个对象,才会创建。这样的实现思想或者说是原理都是iOS开发中非常重要的,所以。懒汉式的单例模式也是最为重要的,是开发中最常见的。
2、饿汉式:在没有使用代码去创建对象之前,这个对象已经载入好了,而且分配了内存空间,当你去使用代码创建的时候,实际上仅仅是将这个原本创建好的对象拿出来而已。
接下来介绍的就是饿汉式:
刚刚在分析饿汉式和懒汉式的特点时提到过。饿汉式是在使用代码去创建对象之前就已经创建好了对象,这里提到的使用代码去创建对象实际上就是用alloc或者是对外暴露的shared方法。最根本上是调用了alloc方法,所以。换句话说。饿汉式也就是在我们手动写代码去alloc之前就已经将对象创建完成了。此时我们就要思考了,什么方法可以实现这种效果呢?这里介绍两个方法,第一个是load方法,第二个是initialize方法
1、load方法:当类载入到执行环境中的时候就会调用且仅调用一次,同一时候注意一个类仅仅会载入一次(类载入有别于引用类,能够这么说,全部类都会在程序启动的时候载入一次,无论有没有在眼下显示的视图类中引用到)
2、initialize方法:当第一次使用类的时候载入且仅载入一次
我们以load方法作为示范,在project中再次创建一个新的类NTMusicPlayer,做一些主要的同样操作,在.h文件里暴露出sharedMusicPlayer方法。在.m文件里利用static定义一个全局变量musicPlayer,
接着我们须要写出load方法
+ (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;
}
到这里。饿汉式的解说也完毕,以下是整合代码
#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
再次新建一个类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情况的单例模式
我们知道,在MRC模式也就是非ARC模式中。我们是须要手动去管理内存的,因此,我们能够使用release去将一个对象手动销毁。那么这种话。我们的创建出来的单例对象也能够被非常轻易的销毁。所以在非ARC情况下的单例模式,我们将着重将目光放到内存管理的方法上去,首先我们能够先思考下。有哪些方法是用来进行内存管理的。
这里就列举出来了:release、retain、retainCount、autorelease。以下就分别进行重写并说明(以上述GCD版为例):
1、首先是release方法,我们是不希望将我们的单例对象进行销毁掉的,那么非常easy。重写release(须要将环境变为MRC,不然使用这些方法会报错)
- (oneway void)release
{
}
括号里的參数是系统生成的,我们仅仅须要将这种方法重写。然后不在里面写代码就能够了
2、retain方法:在这里面仅仅须要返回这个单利本身就好了,不正确引用计数做不论什么处理
- (instancetype)retain
{
return picturePlayer;
}
3、retainCount方法。这种方法返回的是对象的引用计数,我们已经重写了retain方法,不希望改变单例对象的引用计数,所以在这里返回1就好了
- (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方法。而在父类中。仅仅会存在一次创建对象,创建之后则是直接返回了创建好的那个单例。通俗来说,当一个子类创建单例对象的时候。调用到了父类的创建方法,获取到了这个单例对象,但假设第二个子类再创建单例对象,调用到父类的创建方法,这时候进行的操作不再是创建新的对象,而是返回第一个子类创建的对象。
所以,这种利用继承关系来简化的方法是不可取的。
那么这个时候我们便能够考虑利用宏定义来进行代码的简化,由于我们比較刚刚写的三个单例类来说。代码有非常大的相似度。我们能够抽取这些代码将他们定义成宏。在project中创建一个专门放置宏的.h文件。创建方法是,新建文件->在iOS模块中选择Other->Empty->在Save As中填写类的名字,可是要记着加后缀.h->最后点击Create
这时候我们须要去分析下应该怎么去抽出代码,毕竟从刚刚所写的三个类还是有些区别,通过比較我们能够发现,有这些地方是不同的
在.h文件里:shared后面的名字是不同的
在.m文件里:定义的全局变量名字是不同的,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;\
}
相比之前做了一些细节的优化,首先将全局变量的名字改为了instance,这样对于全部的类都是可以共用的,然后利用了括号和#号的联系来使宏定义变的灵活,我们使用的时候在宏定义的括号里敲出我们的单例对象名字就好了(注意因为name这个属性是直接拼接在了shared后面,所以我们在括号里写单例的名字的时候应该将首字母大写),最后要注意一点细节,对于非常大一段代码,直接放到宏中是不可以识别的。所以这里我们须要使用 \ 这个符号,这个符号表示后面的一段是属于宏定义中的,所以我们在每条代码前面都加入上了这个符号。
这是ARC情况下的单例模式,那么在非ARC情况下的单例模式我们也要将其定义出来。再次用上述方法创建一个.h文件NTSingleton_MRC.h
// .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,然后跳转到对应的代码进行执行.。
讲到这里。可能会想到对于MRC和ARC两个封装类来说,不同的地方就仅仅是在于MRC加入了4个方法而已。那么我们就能够这样做,使用条件编译将这四个方法包装起来,检測到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
假设你看到了这里。我想,你对单例模式的掌握应该更深了^-^