美文网首页
单例设计模式及其内部实现特点说明

单例设计模式及其内部实现特点说明

作者: M_PI_4 | 来源:发表于2020-03-30 07:43 被阅读0次

    1.什么是单例

    确保对于一个给定的类只有一个实例存在,这个实例有一个全局唯一的访问点。
    可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问,从而方便地控制了实例个数,并节约系统资源。

    2.单例能做什么

    • 在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次),这个类创建出来的对象永远只有一个。
      使用场景:
    1. 工具类
    2. 频繁创建的类(比如登录页,未登录状态下,某些触发条件会调出登录页)

    3.单例的实现

    3.1 实现的方式
    1> 命名方式:

    share+类名 | default + 类名 | share | 类名

    如:+(instancetype)sharedSingle;
    2> 实现代码

    1. 未使用单例:
    RTTool *tool_1 = [[RTTool alloc] init];
    RTTool *tool_2 = [[RTTool alloc] init];
    RTTool *tool_3 = [[RTTool alloc] init];
    NSLog(@"\n tool_1:%p \n tool_2:%p \n tool_3:%p",tool_1,tool_2,tool_3); 
    

    打印对象地址:


    No Single.png

    可以观察到,三份地址都不同。

    1. 使用单例:
      注:还不完善
      互斥锁:
    static RTTool *_instance; //提供全局静态变量(作用域结束,本身不可修改)
    /// 调用alloc真正实现内存分配的方法
    + (instancetype)allocWithZone:(struct _NSZone *)zone {
        @synchronized (self) {
            if(_instance == nil) {
                _instance = [super allocWithZone:zone];
            }
        }
        return _instance;
    }
    

    GCD一次代码:

    static RTTool *_instance; //提供全局静态变量
    /// 调用alloc真正实现内存分配的方法
    + (instancetype)allocWithZone:(struct _NSZone *)zone {
        static dispatch_once_t onceToken;
        // 参数取地址的好处?
        dispatch_once(&onceToken, ^{
            _instance = [super allocWithZone:zone];
        });
        return _instance;
    } 
    

    打印对象地址:

    Single.png
    可以观察到,三份地址完全相同

    提供可供外界访问的便捷方法

    static RTTool *_instance; //提供全局静态变量
    + (instancetype)shareManager {
        static dispatch_once_t onceToken;
        // 参数取地址的好处?
        dispatch_once(&onceToken, ^{
            _instance = [[self alloc] init];
        });
        return _instance;
    }
    //+ (instancetype)shareManager {
    //    return [[self alloc] init];
    //}
    

    然后我们就可以这样调用

    RTTool *tool_1 = [[RTTool alloc] init];
    RTTool *tool_2 = [[RTTool alloc] init];
    RTTool *tool_3 = [[RTTool alloc] init];
    RTTool *tool_4 = [RTTool shareManager]; //便捷访问方法
    NSLog(@"\n tool_1:%p \n tool_2:%p \n tool_3:%p \n tool_4:%p",tool_1,tool_2,tool_3,tool_4);
    

    完整的Singleton实现方式
    需要遵守拷贝协议

    @interface RTTool()<NSCopying,NSMutableCopying>
    
    static RTTool *_instance; //提供全局静态变量
    + (instancetype)shareManager {
        static dispatch_once_t onceToken;
        // 参数取地址的好处?
        dispatch_once(&onceToken, ^{
            _instance = [[self alloc] init];
        });
        return _instance;
    }
    //+ (instancetype)shareManager {
    //    return [[self alloc] init];
    //}
    /// MARK: - 拷贝操作的时候,我们的_instance已经存在了,直接返回给调用方即可
    - (id)copyWithZone:(NSZone *)zone {
        return _instance;
    }
    - (id)mutableCopyWithZone:(NSZone *)zone {
        return _instance;
    }
    

    调用及结果:


    Single.png

    3.2 static的作用
    编译时分配内存,程序结束时释放内存单元。
    3.2.1 作用于变量
    作用范围:本文件内部有效/本作用域有效。

    • 1.局部变量
      改变变量的存储方式(生命期),使变量成为静态的局部变量。即编译时就为变量分配内存,直到程序退出才释放存储单元。
      延长局部变量的生命周期,作用范围为局部变量作用域。
      好处:使得该局部变量有记忆功能,可以记忆上次的数据,不过由于仍是局部变量,因而只能在代码块内部使用(作用域不变)。
    { 
      static int a; //作用范围为本代码块结束
    } 
    
    • 2.全局变量
      限制某些外部变量的作用域,使其只在本文件中有效,而不能被其他文件引用。(本文单例模式static全局变量的作用即此)

    3.2.2 作用于函数
    使用static用于函数定义时,对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。

    如果想要其他文件可以引用本地函数,则要在函数定义时使用关键字extern,表示该函数是外部函数,可供其他文件调用。另外在要引用别的文件中定义的外部函数的文件中,使用extern声明要用的外部函数即可。

    3.3 @synchronized作用
    @synchronized 的作用是创建一个互斥锁,保证此时没有其它线程对self对象进行修改,保证代码的安全性。也就是包装这段代码是原子性的,安全的。这个是objective-c的一个锁定令牌,防止self对象在同一时间内被其他线程访问,起到保护线程安全的作用。
    更多使用细节可查看链接
    我们在此处不推荐使用@synchronized的原因(更多原因点击查看):

    • 1.@synchronized速度慢,消耗CPU资源比较大。
    • 2.外部调用可能导致死锁。

    3.4 dispatch_once为什么能保证线程安全,参数取地址的好处

      1. dispatch_once为什么能保证线程安全
        核心函数dispatch_once_f:
    void dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)){
        volatile long *vval = val;
        if (dispatch_atomic_cmpxchg(val, 0l, 1l)) {
            func(ctxt); // block真正执行
            dispatch_atomic_barrier();
            *val = ~0l;
        } 
        else 
        {
            do
            {
                _dispatch_hardware_pause();
            } while (*vval != ~0l);
            dispatch_atomic_barrier();
        }
    } 
    

    参见dispatch_once源码,其核心函数dispatch_once_f源码分析:
    某一个线程A首次进入dispatch_once_f,执行传入dispatch_once_f的block,然后调用dispatch_atomic_barrier;dispatch_atomic_barrier是一种内存屏障,串行化读写操作和解决顺序一致性问题,等屏障前的指令执行完了,屏障后的指令才能开始执行。
    在首个线程A执行block的过程中,如果其它的线程也进入dispatch_once_f,执行方法就会走到else分支,调用_dispatch_hardware_pause,它干的事情就是延迟空等,这样做有助于提高性能和节省CPU耗电。
    首个线程已经将block执行完毕,dispatch_atomic_barrier退出,这就保证了在dispatch_once_f的block的执行的唯一性,生成的单例也是唯一的。
    注:dispatch_once的缺点

    3.5 NSCopying,NSMutableCopying
    这部分很简单详情见链接

    4.单例的优缺点

    更多说明:参看链接
    优点 :
    1、由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
    2、由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决;
    3、单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
    4、单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

    缺点 :
    1、单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
    2、单例模式与单一职责原则有冲突。

    5.总结

    除单一设计原则与允许可扩展需求外,单例设计在性能优化以及减少内存占用方面具有非常良好的表现。

    参考资源

    1. 参数问题:https://www.jianshu.com/p/bb0d47e0b26e
    2. 单例优劣:https://www.jianshu.com/p/1aaa1256d641

    相关文章

      网友评论

          本文标题:单例设计模式及其内部实现特点说明

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