iOS 伪单例模式

作者: 纯洁的小袋子 | 来源:发表于2017-04-10 11:28 被阅读169次

    本文仅探讨 iOS 中单例的适用场景及生命周期管理,如需单例教程及其定义作用的请访问:设计模式系列14--单例模式

    最近在做项目的重构工作,翻看了一下源码,发现了各种历史遗留问题。其中随处可见的单例,产生了万物皆单例的现象(说好的万物皆对象呢?)。

    在与前开发人员沟通后,对方坚持使用单例的原因如下:

    • 代码简洁,不需要声明属性以及创建新的实例对象,需要的时候就可以马上调用。
    • 方便管理对象的生命周期,把对象的创建和销毁时机都掌握在开发人员手中,可以控制对象的销毁时机。
    • 历史遗留,iOS 系统类中随处可见的单例,我们的前辈们也都是这么用的,那就这么干吧。

    第一点无法反驳,单例确实很好用,写起来有种欲仙欲死的快感。但是,不管副作用的话,毒品产生的快感大概比这更甚吧。作为一个有追求的程序猿,怎么能被普通的感官快感所诱惑,我们的目标是星辰大海好吗。

    第二点无法直视,既然是单例为什么要手动销毁呢。这时候就有人说了,比如退出登录后,需要把账户的单例销毁。作为需要全局使用的对象,这样的需求确实无可厚非,那么如果这个单例对象只是在一个地方使用到了呢?需要特地建一个单例并手动去管理单例的释放时机吗?这还是单例吗,这是假单例吧。

    真单例

    吐槽完毕。进入正题,单例作为一个变态的全局变量,首先看他的定义:

    保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    那么他的使用场景很简单且很明确:

    • 在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在 APP 开发中我们可能在任何地方都要使用用户的信息,那么可以在登录的时候就把用户信息存放在一个文件里面,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

    • 有的情况下,某个类可能只能有一个实例。比如说你写了一个类用来播放音乐,那么不管任何时候只能有一个该类的实例来播放声音。再比如,一台计算机上可以连好几个打印机,但是这个计算机上的打印程序只能有一个,这里就可以通过单例模式来避免两个打印任务同时输出到打印机中,即在整个的打印过程中我只有一个打印程序的实例。

    综上所述,不遵守以上定义的单例都是伪单例,例如用户信息单例就是典型的伪单例。

    伪单例

    使用伪单例并没有什么错,我们不需要咬文爵字,只要有合适的应用场景,并承认自己是伪单例,我们也可以开开心心地使用它。

    那么我们今天就来好好谈谈伪单例的正确使用姿势(不管是不是你创造的,既然接盘了你就要负责到底)。

    首先本文中对伪单例的定义:

    需要管理生命周期,并且长时间不需要销毁的单例对象。

    即在单例对象的基础上,需要对其生命周期进行管理,并且在应用启动期间如没有特殊情况,会一直存活。

    伪单例的销毁

    伪单例的销毁要基于其创建的方式,常规的有两种:同步锁、GCD。

    static InstanceSync *instance = nil;
    @implementation InstanceSync
    // 同步锁方式
    +(instancetype)shareInstance{
        @synchronized (self) {
            if (!instance) {
                instance = [[self alloc]init];
            }
        }
        return instance;
    }
    
    static InstanceSync *instance = nil;
    static dispatch_once_t onceToken;
    @implementation InstanceSync
    // GCD 方式
    +(instancetype)shareInstance{
        dispatch_once(&onceToken, ^{
            instance = [[self alloc]init];
        });
        return instance;
    }
    

    首先我们使用同步锁的单例来试验一下,一般我们销毁一个对象是将其置为空,即可以释放,如下:

    NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
    InstanceSync *instanceSync = [InstanceSync shareInstance];
    instanceSync = nil;
    NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
    

    实际上,这样并不能销毁这个对象:

    2017-04-10 10:54:10.449 instanceSync : <InstanceSync: 0x600000016ea0>
    2017-04-10 10:54:10.449 instanceSync : <InstanceSync: 0x600000016ea0>
    

    其实在常规单例的内部都有一个全局静态变量,我们需要对其置空才能释放该单例对象:

    -(void)destoryInstance{
        instance = nil;
    }
    
    -(void)dealloc{
        NSLog(@"%@ occur",NSStringFromSelector(_cmd));
    }
    
    

    那么我们再来尝试一下:

    NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
    InstanceSync *instanceSync = [InstanceSync shareInstance];
    [instanceSync destoryInstance];
    NSLog(@"instanceSync : %@",[InstanceSync shareInstance]);
    
    2017-04-10 11:05:22.112  instanceSync : <InstanceSync: 0x608000200480>
    2017-04-10 11:05:22.112  instanceSync : <InstanceSync: 0x600000200430>
    2017-04-10 11:05:24.366  dealloc occur
    

    可以看到伪单例对象 [InstanceSync shareInstance] 并没有马上进入 dealloc,而是在打印完第二 log 后才进入 dealloc;因此这里需要注意:

    如果伪单例对象被外部变量所持有,那么在释放单例对象时,需要确保所有持有变量都被释放后,才可以进入单例的释放。因此不建议将单例赋值给外部变量,以免无法在预期内释放单例对象。

    此外再次调用 [InstanceSync shareInstance] 将会产生新的对象,这也是易于理解的,那么如果使用 GCD 的方式能否产生新的对象?

    实际上,这就取决于你销毁对象的方式:

    -(void)destoryInstance{
        instance = nil; // 销毁静态全局变量
        onceToken = nil; // 销毁 GCD onceToken
    }
    

    如果只销毁静态全局变量,那么调用该方法后,将不会产生新的对象:

    2017-04-10 11:21:37.917  instanceGCD : <InstanceGCD: 0x60000000d700>
    2017-04-10 11:21:37.918  instanceGCD : (null)
    2017-04-10 11:21:37.918  dealloc occur
    

    如果销毁 GCD onceToken ,那么不论销毁静态全局变量,都会产生新的对象。

    结束

    实际上,本文讲述的是在明知是伪单例的情况下,如何正确地管理伪单例的生命周期,文中若有不实之处,希望大家提出宝贵的意见。

    相关文章

      网友评论

        本文标题:iOS 伪单例模式

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