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
假设你看到了这里。我想,你对单例模式的掌握应该更深了^-^
iOS单例设计模式具体解说(单例设计模式不断完好的过程)的更多相关文章
- 设计模式——通用泛型单例(普通型和继承自MonoBehaviour)
单例模式是设计模式中最为常见的,不多解释了.但应该尽量避免使用,一般全局管理类才使用单例. 普通泛型单例: public abstract class Singleton<T> where ...
- java23种设计模式之二: 单例设计模式(6种写法)
目的:在某些业务场景中,我们需要某个类的实例对象的只能有一个,因此我们需要创建一些单例对象. 本文共有6种写法,仅供参考 1.饿汉式 优点: 在多线程情况下,该方法创建的单例是线程安全的(立即加载) ...
- Java设计模式:Singleton(单例)模式
概念定义 Singleton(单例)模式是指在程序运行期间, 某些类只实例化一次,创建一个全局唯一对象.因此,单例类只能有一个实例,且必须自己创建自己的这个唯一实例,并对外提供访问该实例的方式. 单例 ...
- Scala 深入浅出实战经典 第79讲:单例深入讲解及单例背后的链式表达式
王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...
- [转]SpringMVC<from:form>表单标签和<input>表单标签简介
原文地址:https://blog.csdn.net/hp_yangpeng/article/details/51906654 在使用SpringMVC的时候我们可以使用Spring封装的一系列表单标 ...
- Python接口测试实战4(下) - 框架完善:用例基类,用例标签,重新运行上次失败用例
如有任何学习问题,可以添加作者微信:lockingfree 课程目录 Python接口测试实战1(上)- 接口测试理论 Python接口测试实战1(下)- 接口测试工具的使用 Python接口测试实战 ...
- Angular11 模板表单、响应式表单(自定义验证器)、HTTP、表单元素双向绑定
1 模板表单 模型通过指令隐式创建 技巧01:需要在模块级别引入 FormsModule ,通常在共享模块中引入再导出,然后在需要用到 FormsModule 的模块中导入共享模块就可以啦 impor ...
- form表单中只有一个input时,按回车键后表单自动提交(form表单的一个小坑)
form中只有一个input按回车键表单会自动提交 在一个form表单中,若只有一个input,按回车键表单会自动提交,但是当表单中存在多个input时,按回车键不会执行任何操作,这是form表单的一 ...
- js实现表单验证 常用JS表单验证
CSS代码 @charset "gb2312"; /* CSS Document */ body,dl,dt,dd,div,form {padding:;margin:;} #he ...
随机推荐
- CLUSTER - 根据一个索引对某个表集簇
SYNOPSIS CLUSTER indexname ON tablename CLUSTER tablename CLUSTER DESCRIPTION 描述 CLUSTER 指示PostgreSQ ...
- xxtea 文件加密与解密
加密 cocos luacompile -s src -d dst_dir -e -b xxxxx -k xxxxx --disable-compile 解密 cocos luacompile -s ...
- 第一天 初识Python
Python基础 一 编程语言 什么是编程语言? 上面提及的能够被计算机所识别的表达方式即编程语言,语言是沟通的介质,而编程语言是程序员与计算机沟通的介质.在编程的世界里,计算机更像是人 ...
- vue脚手架引入swiper
方法一: 下载swiper: npm install swiper --save-dev swiper4.0使用入口:http://www.swiper.com.cn/usage/index.html ...
- JS和C#方法相互调用
JS和C#方法相互调用 1.JS调用C#后台方法 方法一: 1.首先建立一个按钮,在后台将调用或处理的内容写入button_click中;2.在前台写一个js函数,内容为document.getEle ...
- 题解 洛谷P4035/BZOJ1013【[JSOI2008]球形空间产生器】
题目链接在这QvQ "你要求出这个n维球体的球心坐标",这使我想到的解方程...... 先假设n=2,这是一个二维平面.设圆心的坐标为\((x,y)\),有两个坐标\((a_1,b ...
- yii1框架,事务使用方法
Yii1框架事务操作方法如下: $transaction= Yii::app()->db->beginTransaction();//创建事务 $transaction->commi ...
- 自动清除日期目录shell脚本
很多时候备份通常会使用到基于日期来创建文件夹,对于这些日期文件夹下面又有很多子文件夹,对于这些日期文件整个移除,通过find结合rm或者delete显得有些力不从心.本文提供一个简单的小脚本,可以嵌入 ...
- 有向图连通分量SCC
在无向图中,如果从顶点vi到顶点vj有路径,则称vi和vj连通.如果图中任意两个顶点之间都连通,则称该图为连通图,否则,称该图为非连通图,则其中的极大连通子图称为连通分量,这里所谓的极大是指子图中包含 ...
- jQuery的ready与js的load事件的区别
摘自:http://www.cnblogs.com/see7di/archive/2011/07/15/2239677.html 为了理解这两个事件的异同,读者应该先了解HTML文档加载的顺序. DO ...