iOS中的单例模式

作者: Alexander | 来源:发表于2016-03-23 13:02 被阅读746次

@WilliamAlex大叔

前言

目前流行的社交APP中都离不开单例的使用,我们来举个例子哈,比如现在流行的"糗事百科""美拍"等APP中,当你选择某一个功能时,它都会跳转到登录界面,然而登录界面都是一样的,所以我们完全可以将这个登录控制器设置成一个单例.这样可以节省内存的开销,优化我们的内存,下面纯属个人整理,如果有错误,希望大家指出来,相互进步.下面我们正式开始介绍单例

单例模式

  • 单例模式的作用
  • 确保在程序运行的过程中,一个类或者是一个对象只有一个实例,一个内存,并且该实例易被外界访问.
  • 单例模式的使用场合
  • 在整个应用程序中,共享一份资源,这份资源只需要创建初始化1次,就比如前言中所描述的登录界面.
  • 单例模式的实例
  • 获取主窗口 : [UIApplication sharedApplication]
  • 获取某个目录下的文件资源 : [NSFileManager defaultManager]
  • 数据存储中的偏好设置 : [NSUserDefaults standardUserDefaults]

在写代码之前,我们好好整理整理思路

  • 学习单例的最好方法是从内存地址入手,因为单例的本质是只会创建一份实例,说明它只有一份内存,我们可以通过内存地址触发,慢慢了解单例的好处以及优势.
  • 本章主要介绍两种方式创建单例(使用GCD方式和普通创建单例方式)
  • GCD方式 : dispatch_once_t
  • 普通方式 : if/else语句, @synchronized(加锁)联用

引入单例

  • 我们通过新建一个WGStudent类,在ViewController中创建多个WGStudnt类型的对象,打印出它们的地址
// 不要忘记需要导入头文件哦

- (void)viewDidLoad {
    [super viewDidLoad];
    // 创建对个对象
    WGStudent *student1 = [[WGStudent alloc] init];
    WGStudent *student2 = [[WGStudent alloc] init];
    WGStudent *student3 = [[WGStudent alloc] init];
    WGStudent *student4 = [[WGStudent alloc] init];
    WGStudent *student5 = [[WGStudent alloc] init];

    // 打印对应的地址
    NSLog(@"S1=%p,S2=%p,S3=%p,S4=%p,S5=%p",student1,student2,student3,student4,student5);
}

打印结果

S1=0x7ff4fae07a30
S2=0x7ff4fae0e520
S3=0x7ff4fae04580
S4=0x7ff4fae0e390
S5=0x7ff4fae0e430

  • 总结 : 通过上述示例,每次都会alloc一次,导致它们的内存地址不一样,但是我们最初的目的只是创建同一个对象,我们都知道,只要alloc一次,系统就会开辟一个新的存储空间,但是根据我们的要求,完全是没有必要另辟新的存储空间的.所以这时候我们就需要引入单例模式.

单例模式的原理

  • 原理 : 根据上面的示例,我们可以很清楚的明白,既然我们想要它多次创建,但是只有一份内存,我们只需要重写alloc方法即可吖,在重写的方法中确保进来的对象只创建一次.不错,思路是正确的,但是我们要弄清楚本质,什么才是最严谨的做法.
  • 其实我们这里并不是重写alloc方法,创建对象,调用alloc,其实它的本质是调用了alloc的底层:allocWithZone方法,所以我们实现单例模式重写的是allocWithZone而不是alloc方法.
  • 我们只需要保证整个进程中,allocWithZone只会调用1次即可实现单例模式.
  • 现在我们的目标是将上面的打印中的地址变成同一个内存地址.

创建单例的格式

  • 给外界提供一个接口 :

  • 说明自己的身份,让别人一看就知道它是一个单例

  • 命名规范:share+类名|default+类名|share|类名|standard + 类名

  • 既然做了,我们就要做到最严谨,不管是外界 alloc、init 还是 copy,mutableCopy 都应当只有一份实例重写allocWithZone,让这个方法生成实例的代码只能运行一次即可。

GCD方式 : dispatch_once_t
步骤 :

  • 创建一个WGStudent类
  • 在.h文件中声明一个类方法shareInstance,供给外界使用
  • 在.m文件中,重写allocWithZone方法,保证它在整个进程中只会执行一次.
  • 实现声明的类方法,保证它只会被初始化一次
  • 为了严谨起见,我们重写copyWithZone以及MutableCopyWithZone方法,这里需要注意,重写这两个对象方法时,需要遵循<NSCopying,NSMutableCopying>两个协议,这样才能找到方法,当然,当我们重写这两个方法以后我们可以不遵守这两个协议,去掉也可以(中重写完毕两个对象方法以后).

dispatch_once_t实现单例代码

在WGStudent.h文件中
#import <Foundation/Foundation.h>

@interface WGStudent : NSObject

/**
 *  声明一个类方法,表明自己是一个单例
 */
+ (instancetype)shareInstance;

@end

  • 注意:声明单例的命名规范

  • 注意示例中提出来的问题,下面有详细的解释,先看明白代码.

在WGStudent.m文件中
#import "WGStudent.h"

// 协议可以不遵守吗? (我没有删掉是因为便于理解代码)
@interface WGStudent() <NSCopying, NSMutableCopying>

@end

// onceToken的主要作用是什么?
@implementation WGStudent

// 为什么要定义一个static全局变量?
static WGStudent *_instance;

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    // 这里使用dispatch_once_t的目的是什么?
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        _instance = [super allocWithZone:zone];

    });

    return _instance;
}

+ (instancetype)shareInstance
{
    // 这里使用dispatch_once_t的目的是什么?
    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;
}

@end

打印结果

S1=0x7faceb53b2c0
S2=0x7faceb53b2c0
S3=0x7faceb53b2c0
S4=0x7faceb53b2c0
S5=0x7faceb53b2c0
  • 解释示例中提出来的问题 :
  • 协议可以不遵守吗? 答案是可以的,我们遵守协议的目的主要是重写copyWithZone和mutableCopyWithZone方法(不然打不出方法来),当我们重写完毕之后,就可以不遵守了.
  • onceToken的作用是什么? onceToken的主要作用是用来记录当前的block是否已经执行过了,如果执行过了,那么就不要再次执行.
  • 为什么要定义一个static修饰的全局变量? 使用static修饰全局变量主要是保证只有该文件可以使用,外界是没有办法使用的,防止外界将指针清空(注意: static WGStudent *_instance;是一个被强指针指向的全局变量,既然是单例,就要保证在整个进程中单例对象不要释放,也就是说,单例之所以一直存在,是因为有一个强指针指着),如果指针被清空,下面返回的值就会为nil,没有值,还谈什么单例.
  • 在allocWithZone方法中使用dispatch_once主要是保证,对象只会被创建一次,只分配一次内存.
  • 在shareInstance方法中使用dispatch_once,主要是保证只会初始化一次,比如说:初始化成员属性.为了严谨起见,在类方法中不能直接返回,因为它可能第一次创建,为空返回值就会返回nil.
  • 重写两个对象方法的注意点是什么?前面我们已经说过了,也是为了严谨起见,如果外界使用copy或者mutableCopy创建对象,那我们也将它弄成单例.但是如果你直接敲copy是没有这两个对象方法的,我们必须要遵守<NSCopying,NSMutableCopying>两个协议才能敲出方法,当我们重写完毕时,你可以将协议删掉.

普通方式if来创建单例

首先我们来写一份不够严谨的代码,看看问题出来哪里


#import "WGStudent.h"

@interface WGStudent() <NSCopying, NSMutableCopying>

@end

@implementation WGStudent

// 定义全局变量,保证整个进程运行过程中都不会释放
static WGStudent *_instance;

// 保证整个进程运行过程中,只会分配一个内存空间

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (nil == _instance) {
        _instance = [super allocWithZone:zone];
    }
    return _instance;
}

+ (instancetype)shareInstance
{
    if (nil == _instance) {
        _instance = [[self alloc] init];
    }
    return _instance;
}

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

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}

@end

打印结果

S1=0x7febf153ba80
S2=0x7febf153ba80
S3=0x7febf153ba80
S4=0x7febf153ba80
S5=0x7febf153ba80
  • 注意 : 看到上面的打印结果,咦o,内存地址是一样的,可以了呀,为什么还说不够严谨呢.你丫装逼失败了吧!!!
  • 细心的朋友已经看出来是怎么回事了,用if是不够安全的,我们忽略了多线程这点.
  • 我们来分析一下哈,假如现在有多条线程,假设线程1进入allocWithZone方法中了,判断了一下,咦! 没有值,线程1进来了,有可能线程1还没有赋值,没有分配存储空间,线程2也进入allocWithZone方法了,判断一下,好家伙! 也没有值,这时候线程1已经赋值完毕,分配好了内存空间,线程2也开始了赋值,分配新的内存空间,这就造成了多次分配内存空间,这和单例模式的本质原理是相违背的.
  • 解决办法也很简单,给线程加锁.

解决后的代码

#import "WGStudent.h"

@interface WGStudent()

@end

@implementation WGStudent

// 定义全局变量,保证整个进程运行过程中都不会释放
static WGStudent *_instance;

// 保证整个进程运行过程中,只会分配一个内存空间

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    @synchronized(self) {
        if (nil == _instance) {
            _instance = [super allocWithZone:zone];
        }
        return _instance;
    }
}

+ (instancetype)shareInstance
{
    @synchronized(self) {

        if (nil == _instance) {
            _instance = [[self alloc] init];
        }
        return _instance;
    }

}

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

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}
@end

打印结果

S1=0x7fd39af539d0
S2=0x7fd39af539d0
S3=0x7fd39af539d0
S4=0x7fd39af539d0
S5=0x7fd39af539d0
  • 注意 : 使用线程加锁一定要注意它的位置. 线程加锁的锁对象一般是当前类(self)原因是当前类也是只有一个内存,唯一的.

以上就是实现在ARC环境下创建单例的两种方法

接下来我们来创建MRC环境下的单例

设置环境.png
  • 我们来分析一下哈,ARC与MRC的主要区别是什么(具体的区别后续我会更新的),主要区别就是是否需要手动管理内存.

下面是MRC环境下的代码

在.h文件中声明单例方法
#import <Foundation/Foundation.h>

@interface WGStudent : NSObject

/**
 *  声明一个类方法,表明自己是一个单例
 */
+ (instancetype)shareInstance;

@end
在.m文件中重写方法

#import "WGStudent.h"

@interface WGStudent()

@end

@implementation WGStudent

#pragma mark - ARC环境下的单例
// 定义全局变量,保证整个进程运行过程中都不会释放
static WGStudent *_instance;

// 保证整个进程运行过程中,只会分配一个内存空间

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    @synchronized(self) {
        if (nil == _instance) {
            _instance = [super allocWithZone:zone];
        }
        return _instance;
    }
}

+ (instancetype)shareInstance
{
    @synchronized(self) {

        if (nil == _instance) {
            _instance = [[self alloc] init];
        }
        return _instance;
    }

}

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

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}

#pragma mark - MRC环境下的单例(还要加上上面的方法)

#if __has_feature(objc_arc)
// ARC :就执行上面重写的方法即可
#else
// MRC : 除了执行上面的方法,还需要重写下面的方法.

- (oneway void)release {

    // 什么都不用做,安静的看着其他方法装逼即可
}

- (instancetype)retain
{
    return _instance;
}

- (NSUInteger)retainCount
{
    return MAXFLOAT;
}
#endif

@end

打印结果

S1=0x7f9b6bd90fb0
S2=0x7f9b6bd90fb0
S3=0x7f9b6bd90fb0
S4=0x7f9b6bd90fb0
S5=0x7f9b6bd90fb0
  • 注意点 :
  • 需要将ARC环境设置为MRC环境
  • 示例中我讲ARC和MRC都混合在了一起,需要记住判断当前环境是ARC还是MRC的宏
- (void)currentEnvironment
{
#if __has_feature(objc_arc)
        //  ARC
        NSLog(@"ARC环境");
#else
        //  MRC
        NSLog(@"MRC环境");
#endif
}

以上就是ARC和MRC环境下的单例

  • 在实际开发中,我们为了提高工作效率,一般不会每次需要使用单例时,都老实巴交一步一步的编写单例,我习惯将他们抽取出来,定义成一个宏,到时候使用单例时,直接调用宏,我们只需要传入一个参数.

单例宏代码

// 直接将单例的实现(ARC和MRC)全部定义到PCH文件中,,设置PCH文件路径即可
#define SingleH(instance) +(instancetype)share##instance;

#if __has_feature(objc_arc)
//ARC
#define SingleM(instance) static id _instance;\
\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
+(instancetype)share##instance\
{\
return [[self alloc]init];\
}\
\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}
#else

//MRC
#define SingleM(instance) static id _instance;\
\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
+(instancetype)share##instance\
{\
return [[self alloc]init];\
}\
\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(oneway void)release\
{\
}\
-(instancetype)retain\
{\
    return _instance;\
}\
\
-(NSUInteger)retainCount\
{\
    return MAXFLOAT;\
}
#endif

  • 注意点 :
  • 每一行都需要''不然下一行不能识别
  • 不要在注释后面添加''.否则后面的全部都会变成注释
  • 在实际开发中,我们可以定义的方法不一样,我们可以使用"##"两个井号让方法变成可变的参数,我们传入什么,它就是什么.
  • 注意定义全局变量的时候,我们定义的类是不一样的,所以我们需要将它改为id类型.

这里需要重点听 : 有的初学者朋友可能会使用继承,这样就不用把它定义成宏了,我上面就说过了,我们千万不能在单例中使用继承,原因我们看代码,不要耍流氓

使用继承

  • 使用继承,首先创建一个父类,WGSignaltonTool,在父类的.h文件中声明单例方法,在.m文件中实现单例方法
在.h文件中
#import <Foundation/Foundation.h>

@interface WGSignaltonTool : NSObject

/**
 *  声明一个类方法,表明自己是一个单例
 */
+ (instancetype)shareInstance;

@end


在.m文件中
#import "WGSignaltonTool.h"

@implementation WGSignaltonTool

#pragma mark - ARC环境下的单例
// 定义全局变量,保证整个进程运行过程中都不会释放
static WGSignaltonTool *_instance;

// 保证整个进程运行过程中,只会分配一个内存空间

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    @synchronized(self) {
        if (nil == _instance) {
            _instance = [super allocWithZone:zone];
        }
        return _instance;
    }
}

+ (instancetype)shareInstance
{
    @synchronized(self) {

        if (nil == _instance) {
            _instance = [[self alloc] init];
        }
        return _instance;
    }

}

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

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}

#pragma mark - MRC环境下的单例(还要加上上面的方法)

#if __has_feature(objc_arc)
// ARC :就执行上面重写的方法即可
#else
// MRC : 除了执行上面的方法,还需要重写下面的方法.

- (oneway void)release {

    // 什么都不用做,安静的看着其他方法装逼即可
}

- (instancetype)retain
{
    return _instance;
}

- (NSUInteger)retainCount
{
    return MAXFLOAT;
}
#endif

@end

创建两个子类:WGPerson和WGStudent,分别继承WGSignaltonTool,两个子类只需要继承父类即可,什么都不用写

  • 继承完毕父类,我们来到ViewController.m文件,导入两个子类,然后在ViewDidLoad中打印它们的内存地址.

  • 只打印WGPerson类的地址(单独打印WGStudent类的地址情况和WGPerson类类似,所以,这里就打印一个啦)

NSLog(@"%@,%@",[WGPerson shareInstance],[[WGPerson alloc] init]);

打印结果

<WGPerson: 0x7f9912d93b40>
<WGPerson: 0x7f9912d93b40>
  • 结论 : 感觉使用继承也是可以的吖,打印出来的地址是一样的,我们先别着急,我们接着来看两个一起打印是是什么结果.
NSLog(@"%@,%@",[WGStudent shareInstance],[[WGStudent alloc] init]);
NSLog(@"%@,%@",[WGPerson shareInstance],[[WGPerson alloc] init]);

打印结果

单例[1569:88929] <WGStudent: 0x7f9daa4032f0>,<WGStudent: 0x7f9daa4032f0>
单例[1569:88929] <WGStudent: 0x7f9daa4032f0>,<WGStudent: 0x7f9daa4032f0>

  • 总结 : 你看哈,当我们两个子类一起打印的时候们就会发现,打印出来的结果全是WGStudent,地址也是一样的.这明显是不对的,所以我们千万不能使用继承.

相关文章

  • iOS 单例模式

    关于单例模式的详解,看完这几篇,就完全了然了。iOS 单例模式iOS中的单例模式iOS单例的写法

  • 单例

    iOS单例模式iOS之单例模式初探iOS单例详解

  • 单例模式 Singleton Pattern

    单例模式-菜鸟教程 iOS中的设计模式——单例(Singleton) iOS-单例模式写一次就够了 如何正确地写出...

  • iOS单例模式容错处理

    ios 单例模式容错处理 1、单例模式的使用和问题解决 在ios开发的过程中,使用单例模式的场景非常多。系统也有很...

  • 谈一谈iOS单例模式

    这篇文章主要和大家谈一谈iOS中的单例模式,单例模式是一种常用的软件设计模式,想要深入了解iOS单例模式的朋友可以...

  • iOS 单例模式 or NSUserDefaults

    本文内容:iOS的单例模式NSUserDefaults的使用总结:iOS单例模式 and NSUserDefaul...

  • iOS中的单例模式

    iOS开发中常用到2中设计模式,分别是代理模式和单例模式,本文主要介绍下单例模式 单例模式的作用 可以保证在程序运...

  • 单例

    在iOS开发时,总是会遇到单例模式,单例即是一种模式,更是一种思想,单例模式是借鉴了数学中的单集合。就是一个集合中...

  • IOS 设计模式

    IOS开发中几种设计模式:单例模式、观察者模式、MVC模式、代理模式 一、单例模式 场景:确保程序运行期某个类,只...

  • ios 开发中的单例模式

    其实iOS开发中的单例模式无非就是一个类创建的对象在程序中只有一个对象! iOS中的单例模式有分为赖汉式和饿汉式单...

网友评论

  • SunZzzl:最后说明不能使用继承的时候,打印出来的类名是一样的,这是为什么
    寂静的天空:@DreamsCoder 应为单例是静态的,所有不论之类还是父类共同拥有一份,这里就由父类先拥有,所以打印出来的还是父类的。
  • DamonLu:Good! very 详细!
  • 042a0e1be73f:很详细,谢谢啦!我用dispatch_once_t创建单例,但是不知道为什么要重写allocWithZone?
    迷路的安然和无恙:@阿叔Alex 嗯,如果还想再严谨的话,还可以重写copyWithZone方法
    Alexander:@iiOS 不客气, 因为既然在整个进程过程中只会有一份内存,所以我们需要重写alloc方法(alloc分配内存),而我们在使用alloc创建对象时,其本质是调用了底层的allocWithZone,所以单例一般都是重写allocWithZone,这样更加严谨,更加直接.

本文标题: iOS中的单例模式

本文链接:https://www.haomeiwen.com/subject/boillttx.html