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

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

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

单例模式

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

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

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


    官方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我们发现确实做到了无论创建多少次都是同一个内存地址。


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

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


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


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


测试继承来实现单例
  • 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里。
    • 对于这种情况,最好的设计是在单例设计的时候,初始化的内容不要依赖于其他对象。如果实在要依赖,就不要让它形成环。实在会形成环或者无法控制,就采用异步初始化的方式。先过去,内容以后再填。内部需要做个标识,标识这个单例在造出来之后,不能立刻使用或者完整使用。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,300评论 8 265
  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,222评论 4 34
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 设计模式概述 在学习面向对象七大设计原则时需要注意以下几点:a) 高内聚、低耦合和单一职能的“冲突”实际上,这两者...
    彦帧阅读 3,734评论 0 14
  • 上至达官贵人,下至贩夫走卒,人生最难熬的就是陪孩子高三那一年,轻不得重不得,急不得慢不得。在这一年里,父母仿佛经历...
    青衫居士李君山阅读 3,672评论 2 4