美文网首页设计模式
设计模式第二篇、单例设计模式

设计模式第二篇、单例设计模式

作者: 意一ineyee | 来源:发表于2018-09-13 16:22 被阅读34次
    OC编程之道:设计模式

    目录

    1、什么是单例设计模式
    2、单例设计模式的简单实现
    3、单例设计模式面临的两个问题及其完整实现
    4、单例设计模式的应用场景
    1、什么是单例设计模式

    单例设计模式是指在App的整个生命周期中,某个类的实例只可能被创建一个,同时该类需要向外界提供一个sharedInstance方法来创建这个实例。

    所以单例设计模式有三个设计思想:一不死性,即单例对象的生命周期和App的生命周期一样,只有在App杀死的时候单例对象才会被销毁;二唯一性,即在App的整个生命周期中单例对象只可能被创建一个;三、我们需要向外界提供一个sharedInstance方法来创建这个实例

    2、单例设计模式的简单实现

    要实现单例设计模式,我们只需要抓住它的三个设计思想就可以了。

    • 针对特点三,我们向外界提供一个创建单例的方法
    + (instancetype)sharedSingleton;
    
    • 针对特点一,我们想到全局变量的生命周期就是App的整个生命周期,所以可以采用全局变量来保证单例的不死性

    • 针对特点二,我们想到平常使用的懒加载就能保证只创建一个对象,所以采用懒加载(在这里是判断全局变量是否已经指向了对象)来保证单例的唯一性

    所以写代码如下:

    ProjectSingleton *singleton = nil;
    + (instancetype)sharedSingleton {
        
        if (singleton == nil) {
            
            singleton = [[ProjectSingleton alloc] init];
        }
        
        return singleton;
    }
    

    但是使用全局变量来指向单例对象有一个很明显弱点是:外界使用extern关键字,可以很随便的就能修改掉这个单例对象,类型甚至都可以不是单例类的类型,那这不炸了嘛。如下:

    extern ProjectSingleton *singleton;
    
    singleton = @"222";
    

    你看单例对象本来是ProjectSingleton类的实例,现在外界直接把它改成了字符串,都没报错。所以我们要把全局变量改成静态全局变量,这样这个变量就只能在单例类内部访问了,外界访问会直接报错。

    static ProjectSingleton *singleton = nil;
    + (instancetype)sharedSingleton {
        
        if (singleton == nil) {
            
            singleton = [[ProjectSingleton alloc] init];
        }
        
        return singleton;
    }
    

    这样看起来好像没错了,单例设计模式的三个设计思想就算实现了。

    3、单例设计模式面临的两个问题及其完整实现

    上一小节,我们实现了单例设计模式的三个设计思想,但其实这只能算是简单的实现,因为这样实现单例会面临两个重要的问题:多线程问题多创建入口问题,这两个问题都会导致单例的唯一性被破坏,我们需要慎重解决。

    (1)多线程问题

    如果按照上面的方式实现单例,那当我们的代码中有多个线程同时访问单例的创建方法+ (instancetype)sharedSingleton;时,我们是不能保证单例的唯一性的。比如说线程一已经访问到了singleton = [[ProjectSingleton alloc] init];创建单例对象这一步,但是还没有创建出来,此时线程二恰好正在访问if (singleton == nil)这句代码,那么线程二得到的结果将是YES,也会走创建单例对象的代码,这样我们就会创建两个单例对象,违背了单例设计模式唯一性的设计思想。

    因此,我们需要在懒加载这一步加锁,通过懒加载+锁的组合来保证多线程场景下单例创建的唯一性。当然了加锁的方式也有好几种,如@synchronizedNSLockGCD信号semaphore,由于@synchronized的性能更高,所以我们用它。如下:

    static ProjectSingleton *singleton = nil;
    + (instancetype)sharedSingleton {
        
        @synchronized(self) {
            
            if (singleton == nil) {
                
                singleton = [[ProjectSingleton alloc] init];
            }
        }
        
        return singleton;
    }
    

    但其实我们在这里并没有采用懒加载+锁的方式来保证单例在多线程场景下的唯一性,而是采用GCD的dispatch_once,一方面它是线程安全的(可以替代锁的功能),另一方面它可以保证某段代码在整个App生命周期中只执行一次(可以替代懒加载)。这么做主要是因为用dispatch_once来保证线程安全要比加锁的性能高几十倍(详见此文章),它的性能之所以如此高,是因为它的内部并不是靠pthread等锁来实现的,而是通过大量的原子操作来实现,这些原子操作直接用的是锁的汇编指令,靠CPU指令来支持。这样我们就dispatch_once代替了原来懒加载的设计方案来保证单例的唯一性,也同时保证了多线程场景下单例的唯一性。如下:

    static ProjectSingleton *singleton = nil;
    + (instancetype)sharedSingleton {
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
    
            singleton = [[ProjectSingleton alloc] init];
        });
    
        return singleton;
    }
    
    (2)多创建入口问题

    好了,通过GCD的dispatch_once我们解决了单例创建的唯一性问题,但真得解决完了吗?没有啊,因为别人完全可以不用我们提供的+ (instancetype)sharedSingleton;方法来创建单例,而是用系统提供的alloc、new、copy、mutableCopy来创建,这样的话我们在外界使用单例时是不能完全确保单例的唯一性的。

    因此,我们需要把系统的这些创建入口给封死。我们知道调用alloc、new的时候会调用+ (instancetype)allocWithZone:(struct _NSZone *)zone;方法,调用copy和mutableCopy的时候会调用- (instancetype)copyWithZone:(NSZone *)zone;- (instancetype)mutableCopyWithZone:(NSZone *)zone;方法,所以我们可以在单例类里重写这些方法,让它们调用我们的+ (instancetype)sharedSingleton;方法来返回单例对象就可以了。如下:

    + (instancetype)allocWithZone:(struct _NSZone *)zone {
        
        return [ProjectSingleton sharedSingleton];
    }
    
    - (instancetype)copyWithZone:(NSZone *)zone {
        
        return [ProjectSingleton sharedSingleton];
    }
    
    - (instancetype)mutableCopyWithZone:(NSZone *)zone {
        
        return [ProjectSingleton sharedSingleton];
    }
    

    哇,终于可以松口气了,其实解决了这两个问题之后我们才算完成了单例设计模式的实现。所以一个要实现完整的单例,代码应该如下:

    @implementation ProjectSingleton
    
    static ProjectSingleton *singleton = nil;
    + (instancetype)sharedSingleton {
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
    
            singleton = [[ProjectSingleton alloc] init];
        });
    
        return singleton;
    }
    
    + (instancetype)allocWithZone:(struct _NSZone *)zone {
        
        return [ProjectSingleton sharedSingleton];
    }
    
    - (instancetype)copyWithZone:(NSZone *)zone {
        
        return [ProjectSingleton sharedSingleton];
    }
    
    - (instancetype)mutableCopyWithZone:(NSZone *)zone {
        
        return [ProjectSingleton sharedSingleton];
    }
    
    - (instancetype)init {
        
        self = [super init];
        if (self != nil) {
            
            // 一些属性的设置
        }
        
        return self;
    }
    
    @end
    
    4、单例设计模式的应用场景

    单例设计模式的应用场景也是由它的设计思想所决定的,单例不是具有唯一性和不死性嘛,所以我们可以得到它的应用场景:

    • 用到唯一性:如果我们发现App的生命周期中只需要创建一个某个类的对象,那么这个类可以考虑使用单例设计模式,比如一些工具manager类就常用单例来实现。

    • 用到不死性:如果某些对象的生命周期需要和App一样,或者我们想要延长某些对象的生命周期,就可以考虑使用单例设计模式。

    • 单例传值

      • 一对多传值:如果有一些数据在App中有很多地方都要用到,并且用不着持久化,我们就可以考虑使用单例来传值,这让我们感觉单例传值更像是一种一对多的传值方案。(比如说我们要根据后台返回来的数据动态的更换App的主题色,那么我们就可以把主题色作为一个成员变量放在单例类里,这样我们只需要做一次请求得到颜色,就可以在App的所有界面里使用这个颜色了。)

      • 跨层传值:有的情况下,可能某些数据也并非是很多地方共享,也是一对一的传值,但是由于代理和block传值只适用于近层传值,所以实现不了,此时我们也可以考虑使用单例来实现这种跨层传值。(比如说我们在navigationController栈中第三层的控制器中做操作会影响第一层的某些行为,那么我们使用代理和block传值是无法隔层直接实现的,这是就可以考虑使用单例传值。)

    但是我们也要慎重的使用单例设计模式,不能想什么时候用就什么时候用,特别是不要大材小用,要在真得有必要的时候才用,因为单例对象的生命周期很长,它内部的变量一旦强引用了外部的大对象,这些对象就只能等到App结束时才会被销毁,很容易造成内存问题。

    相关文章

      网友评论

        本文标题:设计模式第二篇、单例设计模式

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