iOS中的单例模式

@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环境下的单例

设置环境.png
  • 我们来分析一下哈,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,地址也是一样的.这明显是不对的,所以我们千万不能使用继承.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 单例模式大概是设计模式中最简单的一个。本来没什么好说的,但是实践过程中还是有一些坑。所以本文小结一下在iOS开发中...
    qiushuitian阅读 3,160评论 1 21
  • 单例模式是日常开发工作中经常会用到的一种设计模式。通过单例模式,可以保证程序中的一个类只有一个实例,从而方便对实例...
    狼凤皇阅读 183评论 0 0
  • 单例模式的作用:保证在程序运行过程中,一个类只有一个实例对象,节约系统资源。 单例模式使用场合:在整个应用程序中,...
    Xcode10阅读 339评论 0 0
  • iOS开发中常用到2中设计模式,分别是代理模式和单例模式,本文主要介绍下单例模式 单例模式的作用 可以保证在程序运...
    Andyzhao阅读 954评论 0 10
  • 晚上回到家里,做的第一件事就是把自己扔到沙发里,懒得动,指使孩子的爸去烧洗脚水。然后打开微信,等着泡脚。 当洗脚水...
    我是慕一阅读 334评论 2 3