iOS中的单例模式

单例模式大概是设计模式中最简单的一个。本来没什么好说的,但是实践过程中还是有一些坑。所以本文小结一下在iOS开发中的单例模式。

一、 什么是单例模式

按照四人帮(GOF)教科书的说法,标准定义是这样的:

Ensures a class has only one instance, and provide a global point of access to it.

保证一个类只有一个实例,并且提供一个全局的访问入口访问这个实例。
然后,类图是这个样子的:


单例类图

什么时候选择单例模式呢?

  • 一个类必须只有一个对象。客户端必须通过一个众所周知的入口访问这个对象。
  • 这个唯一的对象需要扩展的时候,只能通过子类化的方式。客户端的代码能够不需要任何修改就能够使用扩展后的对象。

上面的官方说法,听起来一头雾水。我的理解是这样的。
在建模的时候,如果这个东西确实只需要一个对象,多余的对象都是无意义的,那么就考虑用单例模式。比如定位管理(CLLocationManager),硬件设备就只有一个,弄再多的逻辑对象意义不大。所以就会考虑用单例。

二、 如何实现基本的单例模式?

那么,我们就用Objective-C来实现一下单例模式吧。
要实现比较好的访问,我们就会想到用工厂方法创建对象,提供统一的创建方法的地方给外部使用。要实现仅有一个对象,就会想到用一个全局的东西保存这个对象,然后在创建对象的工厂方法中判断一下,如果对象存在,那么就返回该对象。如果不存在,就造一个返回出去。
于是,基本的单例实现就这样了:

DJSingleton * g_instance_dj_singleton = nil ;
+ (DJSingleton *)shareInstance{
        if (g_instance_dj_singleton == nil) {
            g_instance_dj_singleton = [[DJSingleton alloc] init];
        }
    return (DJSingleton *)g_instance_dj_singleton;
}

看起来不错。不过这个全局的变量 g_instance_dj_singleton有个缺点,就是外面的人随便可以改,为了隔离外部修改,可以设置成静态变量,就是这样子:

1 + (DJSingleton *)shareInstance{
2         static DJSingleton * s_instance_dj_singleton = nil ;
3         if (s_instance_dj_singleton == nil) {
4             s_instance_dj_singleton = [[DJSingleton alloc] init];
5         }
6     return (DJSingleton *)s_instance_dj_singleton;
7 }

单例的核心思想算是实现了。

三、 多线程怎么办?

虽然核心思想实现了,但是依旧不完美。考虑下多线程的情况。即多个线程同时访问这个工厂方法,能够总是保证只创建一个实例对象么?
显然上面的方式是有问题的。比如第一个线程执行到第4行但是还没有进行赋值操作,第二个线程执行第三行。此时判断对象依旧为nil,第二个线程也能往下执行到创建对象操作的第4行。从而创建了多个对象。
那么,如何保证多线程下依旧能够只创建一个呢?这里面的核心思路,是要保证s_instance_dj_singleton这个临界资源的访问(读取和赋值)。
iOS下控制多线程的方式有很多,可以使用NSLock,可以@synchronized等各种线程同步的技术。于是,我们的单例代码变成了这样:

1 + (DJSingleton *)shareInstance{
2         static DJSingleton * s_instance_dj_singleton = nil ;
3           @synchronized(self) {
4                if (s_instance_dj_singleton == nil) {
5                   s_instance_dj_singleton = [[DJSingleton alloc] init];
6               }
7            }
8         return (DJSingleton *)s_instance_dj_singleton;
9 }

看起来多线程没啥问题了了。不过我们可以做的更好。OC的内部机制里有一种更加高效的方式,那就是dispatch_once。性能相差好几倍,好几十倍。关于性能的比对,大神们做过实验和分析。请参考这里
于是,我们的单例变成了这个样子:

+ (DJSingleton *)shareInstance{
    static DJSingleton * s_instance_dj_singleton = nil ;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (s_instance_dj_manager == nil) {
            s_instance_dj_manager = [[DJSingleton alloc] init];
        }
    });
    return (DJSingleton *)s_instance_dj_singleton;
}

四、Objective-C的坑

看起来很完美了。可是Objective-C毕竟是Objective-C。别的语言,诸如C++,java,构造方法可以隐藏。Objective-C中的方法,实际上都是公开的,虽然我们提供了一个方便的工厂方法的访问入口,但是里面的alloc方法依旧是可见的,可以调用到的。也就是说,虽然你给了我一个工厂方法,调皮的小伙伴可能依旧会使用alloc的方式创建对象。这样会导致外面使用的时候,依旧可能创建多个实例。
关于这个事情的处理,可以分为两派。一个是冷酷派,技术上实现无论你怎么调用,我都给你同一个单例对象;一个是温柔派,是从编译器上给调皮的小伙伴提示,你不能这么造对象,温柔的指出有问题,但不强制约束。

1. 冷酷派的实现

冷酷派的实现从OC的对象创建角度出发,就是把创建对象的各种入口给封死了。alloc,copy等等,无论是采用哪种方式创建,我都保证给出的对象是同一个。
由Objective-C的一些特性可以知道,在对象创建的时候,无论是alloc还是new,都会调用到 allocWithZone方法。在通过拷贝的时候创建对象时,会调用到-(id)copyWithZone:(NSZone *)zone-(id)mutableCopyWithZone:(NSZone *)zone方法。因此,可以重写这些方法,让创建的对象唯一。

+(id)allocWithZone:(NSZone *)zone{
    return [DJSingleton sharedInstance];
}

+(DJSingleton *) sharedInstance{
    static DJSingleton * s_instance_dj_singleton = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        s_instance_dj_singleton = [[super allocWithZone:nil] init];
    });
    return s_instance_dj_singleton;
}

-(id)copyWithZone:(NSZone *)zone{
    return [DJSingleton sharedInstance];
}

-(id)mutableCopyWithZone:(NSZone *)zone{
    return [DJSingleton sharedInstance];
}

2. 温柔派的实现

温柔派就直接告诉外面,alloc,new,copy,mutableCopy方法不可以直接调用。否则编译不过。

+(instancetype) alloc __attribute__((unavailable("call sharedInstance instead")));
+(instancetype) new __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) copy __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));

我个人的话比较喜欢采用温柔派的实现。不需要这么多复杂的实现。也让使用方有比较明确的概念这个是个单例,不要调皮。对于一般的业务场景是足够了的。

五、 可不可以再方便点?

可以。
大神们把单例模式的各种套路封装成了宏。这样使用的时候,就不需要每个类都手动写一遍里面的重复代码了。省去了敲代码的时间。
以温柔派的为例,大概是这样子的。

#define DJ_SINGLETON_DEF(_type_) + (_type_ *)sharedInstance;\
+(instancetype) alloc __attribute__((unavailable("call sharedInstance instead")));\
+(instancetype) new __attribute__((unavailable("call sharedInstance instead")));\
-(instancetype) copy __attribute__((unavailable("call sharedInstance instead")));\
-(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));\

#define DJ_SINGLETON_IMP(_type_) + (_type_ *)sharedInstance{\
static _type_ *theSharedInstance = nil;\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
theSharedInstance = [[super alloc] init];\
});\
return theSharedInstance;\
}

那么,在定义和实现的时候就很简单了:

@interface DJSingleton : NSObject
    DJ_SINGLETON_DEF(DJSingleton);
@end

@implementation DJSingleton
    DJ_SINGLETON_IMP(DJSingleton);
@end

六、 单例模式潜在的问题

1. 内存问题

单例模式实际上延长了对象的生命周期。那么就存在内存问题。因为这个对象在程序的整个生命都存在。所以当这个单例比较大的时候,总是hold住那么多内存,就需要考虑这件事了。另外,可能单例本身并不大,但是它如果强引用了另外的比较大的对象,也算是一个问题。别的对象因为单例对象不释放而不释放。
当然这个问题也有一定的办法。比如对于一些可以重新加载的对象,在需要的时候加载,用完之后,单例对象就不再强引用,从而把原先hold住的对象释放掉。下次需要再加载回来。

2. 循环依赖问题

在开发过程中,单例对象可能有一些属性,一般会放在init的时候创建和初始化。这样,比如如果单例A的m属性依赖于单例B,单例B的属性n依赖于单例A,初始化的时候就会出现死循环依赖。死在dispatch_once里。


@interface DJSingletonA : NSObject
    DJ_SINGLETON_DEF(DJSingletonA);
@end

@interface DJSingletonB : NSObject
    DJ_SINGLETON_DEF(DJSingletonB);
@end

@interface DJSingletonA()
@property(nonatomic, strong) id someObj;
@end
@implementation DJSingletonA
DJ_SINGLETON_IMP(DJSingletonA);
-(id)init{
    if (self = [super init]) {
        _someObj = [DJSingletonB sharedInstance];
    }
    return self;
}
@end

@interface DJSingletonB()
@property(nonatomic, strong) id someObj;
@end
@implementation DJSingletonB
DJ_SINGLETON_IMP(DJSingletonB);
-(id)init{
    if (self = [super init]) {
        _someObj = [DJSingletonA sharedInstance];
    }
    return self;
}
@end


//---------------------------------------
DJSingletonA * s1 = [DJSingletonA sharedInstance];
死亡现场

对于这种情况,最好的设计是在单例设计的时候,初始化的内容不要依赖于其他对象。如果实在要依赖,就不要让它形成环。实在会形成环或者无法控制,就采用异步初始化的方式。先过去,内容以后再填。内部需要做个标识,标识这个单例在造出来之后,不能立刻使用或者完整使用。

七、参考资料:

1. Design Patterns -- GOF
2. Pro Objective-C Design Patterns for iOS -- Carlo Chung
3. GCD 中 dispatch_once 的性能与实现

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

推荐阅读更多精彩内容

  • @WilliamAlex大叔 前言 目前流行的社交APP中都离不开单例的使用,我们来举个例子哈,比如现在流行的"糗...
    Alexander阅读 1,905评论 6 28
  • 单例模式是日常开发工作中经常会用到的一种设计模式。通过单例模式,可以保证程序中的一个类只有一个实例,从而方便对实例...
    狼凤皇阅读 185评论 0 0
  • 单例模式的作用:保证在程序运行过程中,一个类只有一个实例对象,节约系统资源。 单例模式使用场合:在整个应用程序中,...
    Xcode10阅读 353评论 0 0
  • 今天文章的主题是:坐在办公室里面工作8小时,你知足吗? “朝八晚六”放下休息时间不算,大概每天工作八小时,实则对于...
    30065阅读 760评论 0 0
  • 彭小六私密群日更计划·关于写作 (1) 作者:陈小星星 、开始写作。 当你决定要写作的时候,就把你想到的所有内容都...
    BigQ个人成长阅读 538评论 0 50