iOS——单例模式

单例模式

优点:

  • 单例可以保证系统中该类有且只有一个实例,便于外界访问,对于项目中个别场景的传值,存储状态更加方便。
  • 程序出了问题,可以快速定位问题所在
  • 由于整个程序中只存在一个对象,因此节省了内存资源,提高程序的运行效率
    缺点:
  • 单例不能被继承,不能有子类,因为它们是共享一份资源的
  • 单例实例一旦创建,对象指针是保存在静态区的,那么在堆区分配空间只有在应用程序终止后才能被释放。单例对象只要程序在运行中就会一直占用系统内存,该对象在闲置的时候不能被销毁,在闲置的时候也消耗了系统的内存资源

两种模式:1、懒汉模式;2、饿汉模式

  • 懒汉模式:第一次用到单例对象的时候再创建
  • 饿汉模式:一进入程序就创建一个单例对象

以上两模式的优缺点

1、时间和空间

比较上面两种写法:懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。

饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。

2、线程安全

(1)从线程安全性上讲,不加同步的懒汉式是线程不安全的

(2)饿汉式是线程安全的,因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。

单例模式的创建

懒汉模式

在最开始的OC学习中,我们学习了一个简单的基本的单例模式的创建:首先定义一个全局变量,类型为我们的类,并给这个全局变量赋值为nil;

当我们获取该类的实例对象的时候,程序会判断该全局变量是不是nil,是就创建该实例并将实例对象赋给全局变量,然后返回该实例对象,否则就直接返回该全局变量。

objectivec 复制代码
#import "AModel.h"

static id _instance = nil;
@implementation AModel

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

@end

但是上面的这个单例模式还是存在一些问题,比如假如到了多线程的环境里,多个进程同时访问单例,该单例模式也有可能返回不同的对象

  • 因此这里我们就要用到***dispatch_once(dispatch_once_t *predicate,dispatch_block_t block);***方法来保证线程安全,因此我们可以这么写:
objectivec 复制代码
#import "AModel.h"

static id _instance = nil;
@implementation AModel

+ (id)sharInstance {
    //定义一个dispatch_once_t类型的名为onceToken的静态全局变量,确保它在运行时只会被初始化一次
    static dispatch_once_t onceToken;
    //调用dispatch_once函数,该函数用于确保一个代码块只执行一次。
    //它接受两个参数:一个指向dispatch_once_t类型变量的指针,以及一个表示需要执行一次的代码块的匿名函数
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}

@end
  • 还有一种防止多线程无法实现单例的方法:就是给代码加一个锁
objectivec 复制代码
#import "AModel.h"

static id _instance = nil;
@implementation AModel

+ (id)sharInstance {
    return [[self alloc] init];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    //@synchronized的作用是创建一个互斥锁,保证此时没有其他线程对self对象进行修改,保证代码的安全性
    @synchronized (self) {
        if (_instance == nil) {
            _instance = [super allocWithZone:zone];
        }
    }
    return _instance;
}

@end

使用以上的方法,可以使我们通过调用该方法来初始化实例对象,但是如果我们使用 [[xxx alloc] init] 方法来初始化该对象,就会发现返回的还是不同的对象,这是因为使用 [[xxx alloc] init] 方法实际上alloc的过程是调用了allocWithZone方法,所以用不了我们自己定义的初始化方法,因此想要真正完成单例模式,我们还应该重写allocWithZone方法,使alloc的时候返回唯一实例;还应该重写 copyWithZone:方法,避免实例对象的 copy 操作导致创建新的对象:

objectivec 复制代码
#import "AModel.h"

static id _instance = nil;
@implementation AModel

+ (id)sharInstance {
    return [[self alloc] init];
}

//将我们原先写在自定义初始化方法中的内容写到allocWithZone中
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [super allocWithZone:zone];
    });
    return _instance;
}

//重写 copyWithZone:方法,避免实例对象的 copy 操作导致创建新的对象
-(instancetype)copyWithZone:(NSZone *)zone
{
    //由于是对象方法,说明可能存在_instance对象,直接返回即可
    return _instance;
}

@end

因为我们在这里已经重写了allocWithZone方法,如果再直接使用[[self alloc] init]就会使程序陷入alloc的死循环,因此这里创建实例对象的时候使用父类的allocWithZone方法

为什么全局变量要使用 static?

① static修饰局部变量

其生命周期与全局变量相同,直到程序结束,只有一份内存空间

作用域不变

② static修饰全局变量

只有一份内存空间

全局变量,在其他文件中,可以通过 extern id _instance来声明,然后直接在其他文件中调用。用 static 修饰后 在其他文件不能通过 extern id _instance 声明后 引用

GCD简化单例(MRC)

在 MRC 环境中,我们需要考虑如果创建出来的单例对象,被手动 release 了怎么办?所以我们在设计单例模式的时候,需要考虑这种情况。如下:

  • retain,单例对象创建后,全局只有一个对象,所以一定要保证 retain 后仍然是自身,且引用计数不变
  • release,由于只有一个对象,被 release 后不能被释放掉,所以 release 操作需要拦截
  • autorelease,与 release 一样
  • retainCount,始终保证引用计数器为1
    所以在 MRC 环境中,设计单例模式时,还需要重写下面四个方法
objectivec 复制代码
//重写 retain 方法,不作计数器加1的操作
-(instancetype)retain
{
    return _instance;
}

//重写 release 方法,不做任何操作
-(void)release
{

}

//重写 autorelease 方法,返回自身
-(instancetype)autorelease
{
    return _instance;
}

//重写 retainCount 方法,返回1
-(NSUInteger)retainCount
{
    return 1;
}

饿汉模式

饿汉模式就是当类第一次被创建的时候就去创建实例对象,并保存在_instance中,由于第一次加载就创建,内存从程序开始运行的时候就分配了,不适合移动设备。

在使用饿汉模式之前,我们先说load方法和initilized方法。

load方法

①当程序刚开始运行的时候,所有的类都会加载到内存中(不管这个类有没有使用),此时就会调用 load 方法

②如果某种操作想要在程序运行的过程中只执行一次,那么这个操作就可以放到 load 方法中

③基于第二点,我们的饿汉模式的单例对象创建就放在 load 方法中

initilized方法

①当类第一次被使用的时候调用(比如,调用类的方法)。

②如果子类没有重写该方法,那么父类的initialized方法可能会被执行多次。所以饿汉模式不能使用这个方法
饿汉模式在类加载的时候就会创建类的实例,而在iOS中,类的实例是在调用init方法时创建的。因此,在饿汉模式中,如果使用init方法来创建实例,就会导致无法创建实例的情况发生。因此,在iOS中,饿汉模式不能使用initilized方法来创建实例。

objectivec 复制代码
static id _instance;
@implementation EHanModel
//当类加载到OC运行环境中(内存)时,就会调用一次(一个类只会加载一次)
+ (void) load {
    _instance = [[self alloc] init];
}

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

+ (id)sharInstance {
    return [[self alloc] init];
}

@end

宏实现单例

由于单例的h文件和m文件一成不变,所以可以抽成宏定义。抽成宏定义需要注意

  1. 宏定义后面如果要替换字符,需要用##拼接
objectivec 复制代码
#define SoundToolH(name) +(instancetype)shared##name;
//调用宏定义SoundToolH(MusicTool)时,就相当于
+(instancetype)sharedMusicTool;
  1. 宏定义后边如果出现换行,需要用符号" \ " 来标记下一行也是宏定义的部分,但最后一行末尾不需要
objectivec 复制代码
#define SoundToolM(name) \
static id _instance;\
 +(instancetype)shared##name\
 {\
    dispatch_once_t onceToken = NULL;\
    dispatch_once(&onceToken)\
    {\
        _instance = [self alloc]init];\
    }\
 }
相关推荐
几维安全44 分钟前
如何保护你的 iOS 应用免受逆向工程攻击
macos·objective-c·cocoa
crasowas18 小时前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store
ii_best20 小时前
ios按键精灵脚本开发:ios悬浮窗命令
ios
Code&Ocean1 天前
iOS从Matter的设备认证证书中获取VID和PID
ios·matter·chip
/**书香门第*/1 天前
Laya ios接入goole广告,开始接入 2
ios
恋猫de小郭2 天前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨2 天前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题2 天前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
重生之我在字节当程序员2 天前
如何实现单例模式?
单例模式
夕泠爱吃糖2 天前
如何实现单例模式?
单例模式