iOS 单例实现方式与优缺点

之前在 实现Singleton 模式——七种实现方式中发现java 的单例有七种实现方式,对里面的懒汉和饿汉模式稍微研究了下,发现IOS 里面也可以对应实现。

简述

面向对象应用程序中的单例类(singleton class)总是返回自己的同一个实例。它提供了对象所提供的资源的全局访问点。与这类设计相关的设计模式称为单例模式。
大家在开发过程中也见过不少的单例,比如UIApplication、UIAccelerometer(重力加速)、NSUserDefaults、NSNotificationCenter,当然,这些是开发Cocoa Touch框架中的,在Cocoa框架中还有NSFileManager、NSBundle等。

1、懒汉模式:实现原理和懒加载其实很像,如果在程序中不使用这个对象,那么就不会创建,只有在你使用代码创建这个对象,才会创建。这种实现思想或者说是原理都是iOS开发中非常重要的,所以,懒汉式的单例模式也是最为重要的,是开发中最常见的。
2、饿汉模式:在没有使用代码去创建对象之前,这个对象已经加载好了,并且分配了内存空间,当你去使用代码创建的时候,实际上只是将这个原本创建好的对象拿出来而已。
3.使用GCD代替手动锁实现单例模式
4.使用宏封装直接便于开发使用

talk is cheap, show me the code 直接上代码展示

1.懒汉模式

static id instance = nil;

// 懒加载 线程不安全 单例
+ (instancetype) ShareInstance
{
    if (instance == nil) {
        instance = [[self alloc] init];
    }
    return instance;
}

// 懒加载  加锁  单例
+ (instancetype) ShareInstance1
{
    @synchronized (self) { //为了线程安全,加上互斥锁
        if (instance == nil) {
            instance = [[self alloc] init];
        }
    }
    return instance;
}

需要的注意点:
1)加synchronized 是为了保证单例的读取线程安全,为什么需要添加synchronized 我已经在之前的文章中 IOS nonatomic 与atomic 分析 描述过此类问题,有趣的是我在网上看到有朋友问:

+(instancetype)sharedSingleton{
static id instance = nil;

  if (!instance) {
    @synchronized (self) {
      instance = [[self alloc] init];
    }
  }
  return instance;
}

这样行吗?

答案是肯定不行的,稍微对synchronized 有点了解就知道这种只是“锁”住了对象的创建,没有“锁”住 if 判断。如果两个线程都进到了 if 里面,一样可以生成两个对象。

2)static
修饰局部变量:修饰了局部变量的话,那么这个局部变量的生命周期就和不加static的全局变量一样了(也就是只有一块内存区域,无论这个方法执行多少次,都不会进行内存的分配),不同的在于作用域仍然没有改变
修饰全局变量:
如果不适用static的全局变量,我们可以在其他的类中使用extern关键字直接获取到这个对象,可想而知,在我们所做的单例模式中,如果在其他类中利用extern拿到了这个对象,进行一个对象销毁,例如:

extern id instance;
instance = nil;

这时候在这句代码之前创建的单例就销毁了,再次创建的对象就不是同一个了,这样就无法保证单例的存在,所以对于全局变量的定义,需要加上static修饰符

  1. allocWithZone 与copyWithZone方法
    我们在项目中一般是直接调用自己定义的类方法:ShareInstance,但是有时候也会调用alloc方法直接对单例进行初始化,那么也会导致没有产生该单例,所以我们需要保证应用中只有一个该类的对象需要重写它的allocWithZone 方法,alloc调用的底层也是allocWithZone方法,直接与上述的单例方法类同。
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
        // 解决多线程问题
        @synchronized(self){
            if (instance == nil) {
                // 调用super的allocWithZone方法来分配内存空间
                instance = [super allocWithZone:zone];
            }
        }
    return instance;
}

如果使用copy创建出新的对象的话,那么就不能够保证单例的存在了也会导致同样的问题。此处直接返回instance就可以了。

- (id)copyWithZone:(NSZone *)zone
{
    return instance;
}

2.饿汉模式

在没有使用代码去创建对象之前,这个对象已经加载好了,并且分配了内存空间,当你去使用代码创建的时候,实际上只是将这个原本创建好的对象拿出来而已。
在alloc之前如何将对象直接赋值呢,有两种方式:load和initialize。具体对于两种方法的描述已经有很多人描述过了,详见: iOS类方法load和initialize详解
大致上就是:
load 会在类加载到运行环境中的时候就会调用且仅调用一次,同时注意一个类只会加载一次(类加载有别于引用类,可以这么说,所有类都会在程序启动的时候加载一次,不管有没有在目前显示的视图类中引用到)
initialize方法:当第一次使用类的时候加载且仅加载一次

static id instance = nil;

+ (void)load
{
    instance = [[self alloc]init];
}

+ (void)initialize
{
    instance = [[self alloc]init];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (instance == nil) {
        instance = [super allocWithZone:zone];
    }
    return instance;
}
+ (instancetype)sharedInstance
{
    return instance;
}
- (id)copyWithZone:(NSZone *)zone
{
    return instance;
}

实际上只需要实现 load 与 initialize 其中一种即可实现单例。

3.使用GCD代替手动锁实现单例模式(推荐使用)

这个在所有的使用者中是最多的,我们使用dispatch_once 方法实现单例模式。
代码如下:

static id instance = nil;

+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc]init];
    });
    return instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc]init];
    });
    return instance;
}

- (id)copyWithZone:(NSZone *)zone
{
    return instance;
}

为什么推荐使用GCD代替手动锁实现单例模式 :
1.写法简单,比起需要手动加锁简单很多。
2.性能优异: @synchronized采用的是递归互斥锁来实现线程安全,而dispatch_once的内部则使用了很多原子操作来替代锁,以及通过信号量来实现线程同步,而且有很多针对处理器优化的地方。
此处有专门的文章细说GCD的优异:
细说@synchronized和dispatch_once

简单的来说:
就是@synchronized 在多线程中加锁,其他线程是等待的,造成了线程资源浪费。
而 dispatch_once主要是根据onceToken的值来决定怎么去执行代码。
1.当onceToken = 0时,线程执行dispatch_once的block中代码
2.当onceToken = -1时,线程跳过dispatch_once的block中代码不执行
3.当onceToken为其他值时,线程被阻塞,等待onceToken值改变

当线程调用shareInstance,此时onceToken = 0,调用block中的代码,此时onceToken的值变为140734537148864。当其他线程再调用shareInstance方法时,onceToken的值已经是140734537148864了,线程阻塞。当block线程执行完block之后,onceToken变为-1.其他线程不再阻塞,跳过block。下次再调用shareInstance时,block已经为-1.直接跳过block。

4.使用宏封装直接便于开发使用

这边就只是简单的告诉你可以通过宏封装单例达到方便直接使用单例。

// .h文件的代码
#define NTSingletonH(name) + (instancetype)shared##name;
// .m文件中的代码(使用条件编译来区别ARC和MRC)
#if __has_feature(objc_arc)

#define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[super alloc]init];\
});\
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;\
}

#else

#define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[super alloc]init];\
});\
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;\
}\
- (oneway void)release\
{\
}\
- (instancetype)retain\
{\
return instance;\
}\
- (NSUInteger)retainCount\
{\
return 1;\
}\
- (instancetype)autorelease\
{\
return instance;\
}

#endif

使用方式就是在新类 NewSingleton 中

@interface NewSingleton : NSObject

NTSingletonH(Manager)

@end

@implementation NewSingleton

NTSingletonM(Manager)

@end

需要的时候简单调用 [NewSingleton sharedManager] 即可

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