美文网首页IOS开发基础(oc)
ios开发基础学习笔记(六)--单例模式

ios开发基础学习笔记(六)--单例模式

作者: miloluo | 来源:发表于2018-05-14 23:25 被阅读0次

    本文转载http://www.cocoachina.com/ios/20171123/21300.html

    单例模式大概是设计模式中最简单的一个。本来没什么好说的,但是实践过程中还是有一些坑。所以本文小结一下在iOS开发中的单例模式。

    一、 什么是单例模式

    按照四人帮(GOF)教科书的说法,标准定义是这样的:

    Ensures a ``class has only one instance, and provide a global point of access to it.

    保证一个类只有一个实例,并且提供一个全局的访问入口访问这个实例。

    然后,类图是这个样子的:

    单例类图.png

    什么时候选择单例模式呢?

    一个类必须只有一个对象。客户端必须通过一个众所周知的入口访问这个对象。

    这个唯一的对象需要扩展的时候,只能通过子类化的方式。客户端的代码能够不需要任何修改就能够使用扩展后的对象。

    上面的官方说法,听起来一头雾水。我的理解是这样的。

    在建模的时候,如果这个东西确实只需要一个对象,多余的对象都是无意义的,那么就考虑用单例模式。比如定位管理(CLLocationManager),硬件设备就只有一个,弄再多的逻辑对象意义不大。所以就会考虑用单例

    二、 如何实现基本的单例模式?

    那么,我们就用Objective-C来实现一下单例模式吧。

    要实现比较好的访问,我们就会想到用工厂方法创建对象,提供统一的创建方法的地方给外部使用。要实现仅有一个对象,就会想到用一个全局的东西保存这个对象,然后在创建对象的工厂方法中判断一下,如果对象存在,那么就返回该对象。如果不存在,就造一个返回出去。

    于是,基本的单例实现就这样了:

    DJSingleton * g_instance_dj_singleton = nil ;
    + (DJSingleton *)shareInstance{
        if(g_instance_dj_singleton == nil) {
            g_instance_dj_singleton = [[DJSingleton alloc] init];
        }
        return` `(DJSingleton *)g_instance_dj_singleton;
    }
    

    看起来不错。不过这个全局的变量 g_instance_dj_singleton有个缺点,就是外面的人随便可以改,为了隔离外部修改,可以设置成静态变量,就是这样子:

    + (DJSingleton *)shareInstance{
        static DJSingleton * s_instance_dj_singleton = nil ;
        if (s_instance_dj_singleton == nil) {
            s_instance_dj_singleton = [[DJSingleton alloc] init];
        } 
        return (DJSingleton *)s_instance_dj_singleton;
    }
    

    单例的核心思想算是实现了。

    三、 多线程怎么办?

    虽然核心思想实现了,但是依旧不完美。考虑下多线程的情况。即多个线程同时访问这个工厂方法,能够总是保证只创建一个实例对象么?

    显然上面的方式是有问题的。比如第一个线程执行到第4行但是还没有进行赋值操作,第二个线程执行第三行。此时判断对象依旧为nil,第二个线程也能往下执行到创建对象操作的第4行。从而创建了多个对象。

    那么,如何保证多线程下依旧能够只创建一个呢?这里面的核心思路,是要保证s_instance_dj_singleton这个临界资源的访问(读取和赋值)。

    iOS下控制多线程的方式有很多,可以使用NSLock,可以@synchronized等各种线程同步的技术。于是,我们的单例代码变成了这样:

    + (DJSingleton *)shareInstance{
        static DJSingleton * s_instance_dj_singleton = nil ;
        @synchronized(self) {
             if (s_instance_dj_singleton == nil) {
             s_instance_dj_singleton = [[DJSingleton alloc] init];
             }
         }
         return (DJSingleton *)s_instance_dj_singleton;
    }
    
    

    看起来多线程没啥问题了了。不过我们可以做的更好。OC的内部机制里有一种更加高效的方式,那就是dispatch_once。性能相差好几倍,好几十倍。关于性能的比对,大神们做过实验和分析。请参考这里

    于是,我们的单例变成了这个样子:

    + (DJSingleton *)shareInstance{
        static DJSingleton * s_instance_dj_singleton = nil ;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            if (s_instance_dj_manager == nil) {
            s_instance_dj_manager = [[DJSingleton alloc] init];
            }
        });
        return (DJSingleton *)s_instance_dj_singleton;
    }
    
    

    四、Objective-C的坑

    看起来很完美了。可是Objective-C毕竟是Objective-C。别的语言,诸如C++,java,构造方法可以隐藏。Objective-C中的方法,实际上都是公开的,虽然我们提供了一个方便的工厂方法的访问入口,但是里面的alloc方法依旧是可见的,可以调用到的。也就是说,虽然你给了我一个工厂方法,调皮的小伙伴可能依旧会使用alloc的方式创建对象。这样会导致外面使用的时候,依旧可能创建多个实例。

    关于这个事情的处理,可以分为两派。一个是冷酷派,技术上实现无论你怎么调用,我都给你同一个单例对象;一个是温柔派,是从编译器上给调皮的小伙伴提示,你不能这么造对象,温柔的指出有问题,但不强制约束。

    1. 冷酷派的实现

    冷酷派的实现从OC的对象创建角度出发,就是把创建对象的各种入口给封死了。alloc,copy等等,无论是采用哪种方式创建,我都保证给出的对象是同一个。

    由Objective-C的一些特性可以知道,在对象创建的时候,无论是alloc还是new,都会调用到 allocWithZone方法。在通过拷贝的时候创建对象时,会调用到-(id)copyWithZone:(NSZone *)zone,-(id)mutableCopyWithZone:(NSZone *)zone方法。因此,可以重写这些方法,让创建的对象唯一。

    +(id)allocWithZone:(NSZone *)zone{
        return [DJSingleton sharedInstance];
    }
    +(DJSingleton *) sharedInstance{
        static DJSingleton * s_instance_dj_singleton = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            s_instance_dj_singleton = [[super allocWithZone:nil] init];
        });
        return s_instance_dj_singleton;
    }
    -(id)copyWithZone:(NSZone *)zone{
        return [DJSingleton sharedInstance];
    }
    -(id)mutableCopyWithZone:(NSZone *)zone{
        return [DJSingleton sharedInstance];
    }
    

    2. 温柔派的实现

    温柔派就直接告诉外面,alloc,new,copy,mutableCopy方法不可以直接调用。否则编译不过。

    +(instancetype) alloc __attribute__((unavailable("call sharedInstance instead")));
    +(instancetype) new __attribute__((unavailable("call sharedInstance instead")));
    -(instancetype) copy __attribute__((unavailable("call sharedInstance instead")));
    -(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));
    

    我个人的话比较喜欢采用温柔派的实现。不需要这么多复杂的实现。也让使用方有比较明确的概念这个是个单例,不要调皮。对于一般的业务场景是足够了的。

    五、 单例模式潜在的问题

    1. 内存问题

    单例模式实际上延长了对象的生命周期。那么就存在内存问题。因为这个对象在程序的整个生命都存在。所以当这个单例比较大的时候,总是hold住那么多内存,就需要考虑这件事了。另外,可能单例本身并不大,但是它如果强引用了另外的比较大的对象,也算是一个问题。别的对象因为单例对象不释放而不释放。

    当然这个问题也有一定的办法。比如对于一些可以重新加载的对象,在需要的时候加载,用完之后,单例对象就不再强引用,从而把原先hold住的对象释放掉。下次需要再加载回来。

    2. 循环依赖问题

    在开发过程中,单例对象可能有一些属性,一般会放在init的时候创建和初始化。这样,比如如果单例A的m属性依赖于单例B,单例B的属性n依赖于单例A,初始化的时候就会出现死循环依赖。死在dispatch_once里。

    相关文章

      网友评论

        本文标题:ios开发基础学习笔记(六)--单例模式

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