今天给同学们讲解一下单例模式在iOS开发中的使用以及单例模式的相关优缺点,那么废话不多说,直接上代码~
- 单例模式介绍和使用场景
- 为什么选择单例模式?
- 实现单例模式思路分析(
核心&掌握
) - 通过@synchronized/dispatch_once 实现单例(
掌握
) - 单例为什么不能通过继承来实现(
掌握
) - 通过宏定义来写一个MRC/ARC环境下的单例(
掌握
) - 单例模式的优缺点(
掌握
) - 单例模式误区(
了解
)
单例模式
- 单例模式的作用
可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问
从而方便地控制了实例个数,并节约系统资源 - 单例模式的使用场合
在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次) - 什么时候选择单例模式呢?(
重点
)-
官方说法
一个类必须只有一个对象。客户端必须通过一个众所周知的入口访问这个对象。
这个唯一的对象需要扩展的时候,只能通过子类化的方式。客户端的代码能够不需要任何修改就能够使用扩展后的对象。 -
个人理解
上面的官方说法,听起来一头雾水。我的理解是这样的。
在建模的时候,如果这个东西确实只需要一个对象,多余的对象都是无意义的,那么就考虑用单例模式。比如定位管理(CLLocationManager),硬件设备就只有一个,弄再多的逻辑对象意义不大。
-
实现单例模式思路分析(核心
)
- 1> 首先我们知道单例模式就是保障在整个应用程序中,一个类只有一个实例,而我们知道创建对象 通过调用alloc init 方法初始化而alloc方法是用来分配内存空间 所以我们就是拦截alloc方法保证只分配一次内存空间。(
核心思路出发点
) -
2> 我们通过查阅官方Api文档如下
- 3> 那么我们就从allocWithZone方法入手但是我们如何保证只创建一个实例对象呢?尤其在多线程的情况下,那么有同学就想到了加锁,iOS中控制多线程的方式有很多,可以使用NSLock,也可以用@synchronized等各种线程同步的技术,代码如下。(
掌握
)
// 该类内的全局变量,外界就不能访问和修改,变量名取_book是为了和该类的其余成员属性区分开!牛逼的大神都这么写 so 建议这么写。
static ZZBook *_book;
@implementation ZZBook
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (_book == nil) {
_book = [super allocWithZone:zone];
}
}
return _book;
}
通过查看log我们发现确实做到了无论创建多少次都是同一个内存地址。
- 4> OC的内部机制里有一种更加高效的方式,那就是dispatch_once。性能相差好几倍,好几十倍。代码如下!关于性能的比对,大神们做过实验和分析。请参考http://blog.jimmyis.in/dispatch_once/。(
掌握
)
// 该类内的全局变量,外界就不能访问和修改,变量名取_person是为了和该类的其余成员属性区分开!牛逼的大神都这么写 so 建议这么写。
static ZZPerson *_person;
@implementation ZZPerson
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_person = [super allocWithZone:zone];
});
return _person;
}
- 5> 到这里我们实现了通过allocWithZone通过加锁只分配一次内存空间但是我们通过观察系统的单例例如UIApplication / NSUserDefaults 等都会提供一个快捷的类方法访问那么我们参照系统的做法,代码如下。(
掌握
)
+ (instancetype)sharedBook
{
@synchronized(self) {
if (_book == nil) {
_book = [[self alloc] init];
}
}
return _book;
}
- 6> Objective-C中构造方法不像别的语言如C++,java可以隐藏构造方法,实则是公开的!由Objective-C的一些特性可以知道,在对象创建的时候,无论是alloc还是new,都会调用到 allocWithZone方法。在通过拷贝的时候创建对象时,会调用到-(id)copyWithZone:(NSZone *)zone,-(id)mutableCopyWithZone:(NSZone *)zone方法。因此,可以重写这些方法,让创建的对象唯一。代码如下!(
掌握
)
//
// ZZBook.m
// 8-多线程技术
//
// Created by Jordan zhou on 2018/11/15.
// Copyright © 2018年 Jordan zhou. All rights reserved.
//
#import "ZZBook.h"
@interface ZZBook()
@end
// 该类内的全局变量,外界就不能访问和修改,变量名取_book是为了和该类的其余成员属性区分开!牛逼的大神都这么写 so 建议这么写。
static ZZBook *_book;
@implementation ZZBook
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (_book == nil) {
_book = [super allocWithZone:zone];
}
}
return _book;
}
+ (instancetype)sharedBook
{
@synchronized(self) {
if (_book == nil) {
_book = [[self alloc] init];
}
}
return _book;
}
- (id)copyWithZone:(NSZone *)zone
{
return _book;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return _book;
}
@end
- 7> 补充第5点我们可以通过重写方法,让创建的对象唯一,我们同样也可以通过编译器告诉外面,alloc,new,copy,mutableCopy方法不可以直接调用。否则编译不过。代码如下!
+(instancetype) alloc __attribute__((unavailable("call sharedBook instead")));
+(instancetype) new __attribute__((unavailable("call sharedBook instead")));
-(instancetype) copy __attribute__((unavailable("call sharedBook instead")));
-(instancetype) mutableCopy __attribute__((unavailable("call sharedBook instead")));
当外部通过如上方法创建时会直接报错如下
- 8> 通过dispatch_once实现单例的代码如下!(
掌握
)
//
// ZZPerson.m
// 8-多线程技术
//
// Created by Jordan zhou on 2018/11/15.
// Copyright © 2018年 Jordan zhou. All rights reserved.
//
#import "ZZPerson.h"
@interface ZZPerson()
@end
// 该类内的全局变量,外界就不能访问和修改,变量名取_person是为了和该类的其余成员属性区分开!牛逼的大神都这么写 so 建议这么写。
static ZZPerson *_person;
@implementation ZZPerson
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_person = [super allocWithZone:zone];
});
return _person;
}
+ (instancetype)sharedPerson
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_person = [[self alloc] init];
});
return _person;
}
- (id)copyWithZone:(NSZone *)zone
{
return _person;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return _person;
}
@end
-
9> 通过对比@synchronized或者dispatch_once实现单例代码,每个类中会发现写的代码都是完全相同的 除了命名的参数不同以及方法名不一样,如下图!
- 10> 承接第9点,那么有人肯定想到那可不可以用继承呢?我们测试通过继承的方式打印如下代码!
#pragma mark - 测试通过继承来实现单例
- (void)singleton3
{
ZZPerson *p1 = [[ZZPerson alloc] init];
ZZPerson *p2 = [ZZPerson sharedInstance];
ZZBook *b1 = [[ZZBook alloc] init];
ZZBook *b2 = [ZZBook sharedInstance];
NSLog(@"%@ - %@ - %@ - %@",p1,p2,b1,b2);
}
结果如下图: 通过打印我们发现书的类型也变成人了 那是因为一次性代码在程序运行过创建一次ZZPerson比ZZBook先创建那么_instance的值永远为ZZPerson类型所以是不能通过继承来实现单例的
- 11> 争取方式通过宏定义来写以后要用直接拖走这个宏就可以!而单例模式在ARC\MRC环境下的写法有所不同,需要编写2套不同的代码!可以用宏判断是否为ARC环境!
#if __has_feature(objc_arc)
// ARC
#else
// MRC
#endif
编写代码如下!
// name是外部传递的参数 ##是拼接符 用来拼接参数
// .h文件
#define ZZSingletonH(name) + (instancetype)shared##name;
// 如何定义一个宏表示后面都属于这个宏 +上" \" 即可表示后面所以的东西都属于这个宏
// .m文件
#if __has_feature(objc_arc) // 是ARC
#define ZZSingletonM(name) \
static id _instance; \
\
+ (instancetype)allocWithZone:(struct _NSZone *)zone \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [super allocWithZone:zone]; \
}); \
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; \
} \
\
- (id)mutableCopyWithZone:(NSZone *)zone \
{ \
return _instance; \
}
#else // 不是ARC
#define ZZSingletonM(name) \
static id _instance; \
+ (instancetype)allocWithZone:(struct _NSZone *)zone \
{ \
if (_instance == nil) { \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [super allocWithZone:zone]; \
}); \
} \
return _instance; \
} \
\
+ (instancetype)shared##name \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [[self alloc] init]; \
}); \
return _instance; \
} \
\
- (oneway void)release \
{ \
\
} \
\
- (id)retain \
{ \
return self; \
} \
\
- (NSUInteger)retainCount \
{ \
return 1; \
} \
+ (id)copyWithZone:(struct _NSZone *)zone \
{ \
return _instance; \
} \
\
+ (id)mutableCopyWithZone:(struct _NSZone *)zone \
{ \
return _instance; \
}
#endif
单例模式的优缺点(掌握
)
使用简单、延时求值、易于跨模块
- 内存占用与运行时间
对比使用单例模式和非单例模式的例子,在内存占用与运行时间存在以下差距:- 单例模式:单例模式每次获取实例时都会先进行判断,看该实例是否存在——如果存在,则返回;否则,则创建实例。因此,会浪费一些判断的时间。但是,如果一直没有人使用这个实例的话,那么就不会创建实例,节约了内存空间。
- 非单例模式:当类加载的时候就会创建类的实例,不管你是否使用它。然后当每次调用的时候就不需要判断该实例是否存在了,节省了运行的时间。但是如果该实例没有使用的话,就浪费了内存。
- 线程的安全性
- 从线程的安全性上来讲,不加同步的单例模式是不安全的。比如,有两个线程,一个是线程A,另外一个是线程B,如果它们同时调用某一个方法,那就可能会导致并发问题。在这种情况下,会创建出两个实例来,也就是单例的控制在并发情况下失效了。
- 非单例模式是线程安全的,因为程序保证只加载一次,在加载的时候不会发生并发情况。
- 单例模式如果要实现线程安全,只需要加上synchronized即可。但是这样一来,就会减低整个程序的访问速度,而且每次都要判断,比较麻烦。
- 双重检查加锁:为了解决如上的繁琐问题,可以使用“双重检查加锁”的方式来实现,这样,就可以既实现线程安全,又能使得程序性能不受太大的影响。
- 单例模式会阻止其它对象实例化其自己的对象的副本,从而确保所有对象都访问唯一实例。
- 因为单例模式的类控制了实例化的过程,所以类可以更加灵活修改实例化过程。
单例模式误区(了解
)
- 内存问题
- 单例模式实际上延长了对象的生命周期。那么就存在内存问题。因为这个对象在程序的整个生命都存在。所以当这个单例比较大的时候,总是hold住那么多内存,就需要考虑这件事了。
- 另外,可能单例本身并不大,但是它如果强引用了另外的比较大的对象,也算是一个问题。别的对象因为单例对象不释放而不释放。
当然这个问题也有一定的办法。比如对于一些可以重新加载的对象,在需要的时候加载,用完之后,单例对象就不再强引用,从而把原先hold住的对象释放掉。下次需要再加载回来。
- 循环依赖问题
- 在开发过程中,单例对象可能有一些属性,一般会放在init的时候创建和初始化。这样,比如如果单例A的m属性依赖于单例B,单例B的属性n依赖于单例A,初始化的时候就会出现死循环依赖。死在dispatch_once里。
- 对于这种情况,最好的设计是在单例设计的时候,初始化的内容不要依赖于其他对象。如果实在要依赖,就不要让它形成环。实在会形成环或者无法控制,就采用异步初始化的方式。先过去,内容以后再填。内部需要做个标识,标识这个单例在造出来之后,不能立刻使用或者完整使用。