iOS中的单例模式

作者:编程    发布时间:2020-01-12 11:51     浏览次数 :

[返回]

不废话,直接看代码:

@WilliamAlex大叔

Singleton pattern:单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。

以上内容来自百度百科。

swift中单例的创建非常简单了,以下从不同的角度来创建单例

前言

目前流行的社交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环境下创建单例的两种方法

  1. 为单例对象实现一个静态实例,设置成 nil。
  2. 调用时检查是否为 nil,是则创建否则直接使用。
  3. 修改alloc等方法,预防被创建新实例。
  4. 本质上,每次调用单例对象的实例方法,就只是创建了指针来指向该单例对象已经分配好的内存。
下一篇:没有了