• iOS——单例模式


    单例模式

    优点:

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

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

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

    以上两模式的优缺点

    1、时间和空间

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

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

    2、线程安全

    (1)从线程安全性上讲,不加同步的懒汉式是线程不安全的
    (2)饿汉式是线程安全的,因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。

    单例模式的创建

    懒汉模式

    在最开始的OC学习中,我们学习了一个简单的基本的单例模式的创建:首先定义一个全局变量,类型为我们的类,并给这个全局变量赋值为nil;
    当我们获取该类的实例对象的时候,程序会判断该全局变量是不是nil,是就创建该实例并将实例对象赋给全局变量,然后返回该实例对象,否则就直接返回该全局变量。

    #import "AModel.h"
    
    static id _instance = nil;
    @implementation AModel
    
    + (id)sharInstance {
        if (_instance == nil) {
            _instance = [[self alloc] init];
        }
        return _instance;
    }
    
    @end
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    • 因此这里我们就要用到***dispatch_once(dispatch_once_t *predicate,dispatch_block_t block);***方法来保证线程安全,因此我们可以这么写:
    
    #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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 还有一种防止多线程无法实现单例的方法:就是给代码加一个锁
    
    #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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    
    #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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    因为我们在这里已经重写了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 环境中,设计单例模式时,还需要重写下面四个方法
    //重写 retain 方法,不作计数器加1的操作
    -(instancetype)retain
    {
        return _instance;
    }
    
    //重写 release 方法,不做任何操作
    -(void)release
    {
    
    }
    
    //重写 autorelease 方法,返回自身
    -(instancetype)autorelease
    {
        return _instance;
    }
    
    //重写 retainCount 方法,返回1
    -(NSUInteger)retainCount
    {
        return 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    饿汉模式

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

    load方法

    ①当程序刚开始运行的时候,所有的类都会加载到内存中(不管这个类有没有使用),此时就会调用 load 方法
    ②如果某种操作想要在程序运行的过程中只执行一次,那么这个操作就可以放到 load 方法中
    ③基于第二点,我们的饿汉模式的单例对象创建就放在 load 方法中

    initilized方法

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

    
    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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    宏实现单例

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

    1. 宏定义后面如果要替换字符,需要用##拼接
    #define SoundToolH(name) +(instancetype)shared##name;
    //调用宏定义SoundToolH(MusicTool)时,就相当于
    +(instancetype)sharedMusicTool;
    
    • 1
    • 2
    • 3
    1. 宏定义后边如果出现换行,需要用符号“ \ ” 来标记下一行也是宏定义的部分,但最后一行末尾不需要
    #define SoundToolM(name) \
    static id _instance;\
     +(instancetype)shared##name\
     {\
        dispatch_once_t onceToken = NULL;\
        dispatch_once(&onceToken)\
        {\
            _instance = [self alloc]init];\
        }\
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    【Redis】使用Java客户端操作Redis
    maven 生命周期 `* `
    C#,入门教程——关于函数参数ref的一点知识与源程序
    Oracle物化视图(Materialized View)
    DeepMind 推出 OPRO 技术,可用于优化 ChatGPT 提示
    日常交流没有障碍,听力就一定正常吗?
    LeetCode 1503. 所有蚂蚁掉下来前的最后一刻【脑筋急转弯】1618
    云原生之容器编排实践-minikube传递秘钥使用阿里云私有镜像仓库
    【随笔】VRRP+MSTP
    基于Ruoyi和WebUploader的统一附件管理扩展(上)
  • 原文地址:https://blog.csdn.net/m0_73348697/article/details/132909755