单例模式(Singleton)

系列文章目录

第一章 OC之单例模式
第二章 OC之对象初始化
第三章 OC之类和协议
第四章 OC之main函数的操作和一些基础概念

前言

提示:本篇我们谈一下关于单例模式


一些关于单例的概念

定义:

如果一个类始终只能创建一个实例,则这个类被称为单例类。
有一个全局的接口来访问这个实例。当第一次载入的时候,它通常使用延时加载的方法创建单一实例。
在程序中,一个单例类在程序中只能初始化一次,为了保证在使用中始终都是存在的,所以单例是在存储器的全局区域,在编译时分配内存,只要程序还在运行就会一直占用内存,在APP结束后由系统释放这部分内存
系统方法中,有未知的自动释放池,如果一个对象进行自动释放的话,又可能进入未知的自动释放池,出现内存问题。这就是单例模式不能自动释放的原因

为什么要使用单例模式?

有时候我们需要一个全局的对象,而且要保证全局有且只有一份即可,这时候就需要用到单例设计模式,需要注意:在多线程的环境下做好线程保护。
一般在程序中,经常调用的类,如工具类、公共跳转类等都会采用单例模式

单例模式的结构:

单例类:包含一个实例且能自行创建这个实例的类
访问类:使用单例的类

单例模式的使用场景:

在整个应用程序中,共享一份资源,这份资源只需初始化一次即可。

单例模式的特点:

  1. 单例是一个类,创造出来的对象叫单例对象
  2. 单例对象使用类方法创建
  3. 单例一旦被创造出来,一直到程序结束才会释放。就是只初始化一次
  4. 不用程序员管理内存,随程序的关闭释放

static

在创建单例模式前,我们先来了解一下static关键字

static关键字只能修饰局部变量全局变量和函数
static修饰局部变量表示将该局部变量存储在静态区
修饰全局变量表示限制该全局变量只能在当前文件中访问
修饰函数用于限制函数只能在当前源文件中使用

特点

内存存储区:静态内存存储区在整个项目工程程序运行期间都存在

优点:

  • 全区变量

不会被其他分支文件远程访问,修改
在其他文件中可以使用和static关键字修饰的相同字段,不会冲突

  • 局部变量

作用域仍为局部作用域,当定义该静态局部变量符号或局部函数语句块结束时,作用域随之结束、

  • 函数

静态函数不能被项目工程中其他文件所调用


单例类

UIApplication应用程序实例类
NSNotificationCenter消息中心类
NSFileManager文件管理类
NSUserDefaults应用程序设置
NSURLCache请求缓存类
NSHTTPCookieStorage应用程序cookies池

创建

一个小例子:


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FirstMethod : NSObject
+ (id)instance;
@end

NS_ASSUME_NONNULL_END




#import "FirstMethod.h"

@implementation FirstMethod
static id instance = nil;

+ (id)instance{
    if (!instance){
        instance = [[super allocWithZone:NULL] init];
    }
    return instance;
}
@end




int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"FirstMethod:%d",[FirstMethod instance] == [FirstMethod instance]);
        NSLog(@"%d",[[FirstMethod alloc]init] == [FirstMethod instance]);
    return 0;
}

但是当我们测试一下却发现:

在这里插入图片描述
这种不能算是真正意义上的单例 ,因为类方法和alloc init创建的单例分配的内存可能不一样
因为alloc会执行allocWithZone,所以如果想只分配一次内存就要重写此方法,同时为了严谨,防止copy出现以上问题,还要重写copyWithZone,mutableCopyWithZone

+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    return instance;
}
-(id)copyWithZone:(NSZone *)zone{
    return instance;
}
 
-(id)mutableCopyWithZone:(NSZone *)zone{
    return instance;
}

重写之后,我们可以看到两次创建的对象是一个对象
在这里插入图片描述

懒汉式和饿汉式

定义:

  • 懒汉式:第一次用到类的时候才会去实例化。
  • 饿汉式:在类的加载的时候就去实例化。

特点:

  • 在访问量较大,或者访问线程较多时,才用饿汉式实现。以空间换时间。
  • 访问量较小时,才用懒汉式,以时间换空间。

多线程和单线程:

线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,
即不同的线程可以执行同样的函数。
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,
也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

懒汉式

  • 不使用GCD

这种方法适用单线程下的创建,但如果在多线程下,可能出现多个线程同时进入if语句,创造出多个对象


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FirstMethod : NSObject
+ (id)instance;
@end

NS_ASSUME_NONNULL_END



#import "FirstMethod.h"

@implementation FirstMethod
static id instance = nil;
+ (id)instance{
    if (!instance){
        instance = [[super allocWithZone:NULL] init];
    }
    return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    return instance;
}
@end

allocWithZone:方法。
我们需要保证单例类只有一个唯一的实例,而平时我们在初始化一个对象的时候, [[Class alloc] init],其实是做了两件事。 alloc 给对象分配内存空间,init是对对象的初始化,包括设置成员变量初值这些工作。而给对象分配空间,除了alloc方法之外,还有另一个方法: allocWithZone。
在NSObject 这个类的官方文档里面,allocWithZone方法介绍说,该方法的参数是被忽略的,正确的做法是传nil或者NULL参数给它。
使用alloc方法初始化一个类的实例的时候,默认是调用了 allocWithZone 的方法。于是覆盖allocWithZone方法的原因已经很明显了:为了保持单例类实例的唯一性,需要覆盖所有会生成新的实例的方法,如果有人初始化这个单例类的时候不走[[Class alloc] init] ,而是直接 allocWithZone, 那么这个单例就不再是单例了,所以必须把这个方法也堵上。

我们试试使用@synchronized来给它加锁
@synchronized 可以防止不同的线程同时执行同一段代码,加锁后每次访问该代码都只能一个线程进行访问

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FirstMethod : NSObject
//+ (id)instance;
+ (instancetype)instance2;
@end

NS_ASSUME_NONNULL_END



#import "FirstMethod.h"

@implementation FirstMethod
static id instance = nil;
+ (instancetype)instance2 {
   @synchronized (self) {
       if (!instance) {
           instance = [[super allocWithZone:NULL] init];
       }
   }
   return instance;
}
@end

但是这样的话,不论有没有创建该类的单例对象,都会锁一次,在次优化代码

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FirstMethod : NSObject
//+ (id)instance;
//+ (instancetype)instance2;
+ (instancetype)instance3;
@end

NS_ASSUME_NONNULL_END




#import "FirstMethod.h"

@implementation FirstMethod
static id instance = nil;
+ (instancetype)instance3 {
    if (!instance) {
        @synchronized (self) {
            if (!instance) {
                instance = [[super allocWithZone:NULL] init];
            }
        }
    }
    return instance;
}
@end


  • 使用GCD的dispatch_once

在iOS开发中,我们也经常使用dispatch_once去定义一个单例类,来保证对象的唯一性。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SecondMethod : NSObject

+ (instancetype)GCDInstance;
@end

NS_ASSUME_NONNULL_END




#import "SecondMethod.h"

@implementation SecondMethod

static id instance = nil;
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    return instance;
}

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

dispatch_once是怎样在多线程的情况下保证生成对象的唯一的?

上边的代码中涉及到了dispatch_once_t变量和dispatch_once函数
dispatch_once_t变量
在这里插入图片描述
intptr_t是为了跨平台,其长度总是所在平台的位数,所以用来存放地址。其实就是一个long。所以dispatch_once_t实际上就是一个long

dispatch_once函数
在这里插入图片描述

将onceToken 值进行修改,其默认是0执行完后变为-1,然后每次进行判断这个值,为-1 就返回,同时做了一些编译期的优化

  • dispatch_once无论使用多线程还是单线程,都只执行一次,在安全的前提下也保证了性能。
  • 当onceToken为0时,线程执行dispatch_once的block中的代码
  • 当onceToken为-1时,线程跳过dispatch_once的block中的代码
  • 当onceToken为其他值时,线程被阻塞,等待onceToken值改变

当调用函数+ (instancetype)shareInstance;时,此时onceToken中的值为0,执行dispatch_once的block中的代码,onceToken中的值为其他值;
这时如果其他线程再次调用函数+ (instancetype)instance,由于onceToken中的值为其他值,线程被阻塞;
当block线程执行完block后,onceToken中的值变为-1,其他线程不在阻塞
再次调用函数+ (instancetype)instance时,onceToken为-1,跳过dispath_once中的block代码

饿汉式

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface ThirdMethod : NSObject
+ (instancetype)HungryMethod;
@end

NS_ASSUME_NONNULL_END



#import "ThirdMethod.h"

@implementation ThirdMethod
static id instance;
+ (void)load {
    [super load];
    instance = [[self allocWithZone:NULL] init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    return instance;
}
@end

load方法:当类加载到运行环境中的时候就会调用且仅调用一次,同时注意一个类只会加载一次(类加载有别于引用类,可以这么说,所有类都会在程序启动的时候加载一次,不管有没有在目前显示的视图类中引用到


单例模式的优缺点

优点:

  • 提供了对唯一实例的受控访问
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,提高系统性能
  • 允许可变数量的实例

缺点:

  • 由于单例模式中没有抽象层,无法继承,不能被重写或拓展(可以使用分类)
  • 单例类的职责中,在一定程度上违背了“单一职责原则”
  • 滥用单例类将带来一些负面问题
    如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
    如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山河丘壑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值