iOS开发-单例(粒)模式的介绍和实战使用

今天给同学们讲解一下单例模式在iOS开发中的使用以及单例模式的相关优缺点,那么废话不多说,直接上代码~

  • 单例模式介绍和使用场景
  • 为什么选择单例模式?
  • 实现单例模式思路分析(核心&掌握
  • 通过@synchronized/dispatch_once 实现单例(掌握
  • 单例为什么不能通过继承来实现(掌握
  • 通过宏定义来写一个MRC/ARC环境下的单例(掌握
  • 单例模式的优缺点(掌握
  • 单例模式误区(了解

单例模式

  • 单例模式的作用
    可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问
    从而方便地控制了实例个数,并节约系统资源
  • 单例模式的使用场合
    在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次)
  • 什么时候选择单例模式呢?(重点
    • 官方说法
      一个类必须只有一个对象。客户端必须通过一个众所周知的入口访问这个对象。
      这个唯一的对象需要扩展的时候,只能通过子类化的方式。客户端的代码能够不需要任何修改就能够使用扩展后的对象。
    • 个人理解
      上面的官方说法,听起来一头雾水。我的理解是这样的。
      在建模的时候,如果这个东西确实只需要一个对象,多余的对象都是无意义的,那么就考虑用单例模式。比如定位管理(CLLocationManager),硬件设备就只有一个,弄再多的逻辑对象意义不大。

实现单例模式思路分析(核心

  • 1> 首先我们知道单例模式就是保障在整个应用程序中,一个类只有一个实例,而我们知道创建对象 通过调用alloc init 方法初始化而alloc方法是用来分配内存空间 所以我们就是拦截alloc方法保证只分配一次内存空间。(核心思路出发点
  • 2> 我们通过查阅官方Api文档如下

    《iOS开发-单例(粒)模式的介绍和实战使用》 官方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我们发现确实做到了无论创建多少次都是同一个内存地址。

《iOS开发-单例(粒)模式的介绍和实战使用》 ZZBook基本打印

  • 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()<NSCopying,NSMutableCopying>

@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")));

当外部通过如上方法创建时会直接报错如下

《iOS开发-单例(粒)模式的介绍和实战使用》 提示编译报错

  • 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()<NSCopying,NSMutableCopying>

@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实现单例代码,每个类中会发现写的代码都是完全相同的 除了命名的参数不同以及方法名不一样,如下图!

    《iOS开发-单例(粒)模式的介绍和实战使用》 2种实现单例的对比

  • 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类型所以是不能通过继承来实现单例的

《iOS开发-单例(粒)模式的介绍和实战使用》 测试继承来实现单例

  • 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里。
    • 对于这种情况,最好的设计是在单例设计的时候,初始化的内容不要依赖于其他对象。如果实在要依赖,就不要让它形成环。实在会形成环或者无法控制,就采用异步初始化的方式。先过去,内容以后再填。内部需要做个标识,标识这个单例在造出来之后,不能立刻使用或者完整使用。
    原文作者:iOS_ZZ
    原文地址: https://www.jianshu.com/p/a55821b2dd5d
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞