iOS开发教程之单例使用问题详析

导语

单例(Singletons),是Cocoa的核心模式之一。在iOS上,单例十分常见,比如:UIApplication,NSFileManager等等。虽然它们用起来十分方便,但实际上它们有许多问题需要注意。所以在你下次自动补全dispatch_once代码片段的时候,想一下这样会导致什么后果。

什么是单例

在《设计模式》一书中给出了单例的定义:

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

单例模式提供了一个访问点,供客户类为共享资源生成唯一实例,并通过它来对共享资源进行访问,这一模式提供了灵活性。

在objective-c中,可以使用以下代码创建一个单例:

+(instancetype)sharedInstance
{
 static dispatch_once_t once;
 static id sharedInstance;
 dispatch_once(&once, ^{
 sharedInstance = [[self alloc]init];
 });
 return sharedInstance;
}

当类只能有一个实例,而且必须从一个访问点对其进行访问时使用单例就显得十分方便,因为使用单例保证了访问点的唯一、一致且为人熟知。

单例中的问题

全局状态

首先我们都应该达成一个共识“全局可变状态”是危险的,因为这样会让程序变得难以理解和调试,就削减状态性代码上,面向对象编程应该向函数式编程学习。

比如下面的代码:

@implementation Math{
 NSUInteger _a;
 NSUInteger _b;
}

-(NSUInteger)computeSum
{
 return _a + _b;
}

这段代码想要计算_a和_B相加的和,并返回。但事实上这段代码存在着不少问题:

  • computeSum方法中并没有把_a和_b作为参数。相比查找interface并了解哪个变量控制方法的输出,查找implementation来了解显得更隐蔽,而隐蔽代表着容易发生错误。
  • 当准备修改_a和_b的值来让它们调用computeSum方法的时候,程序员必须清楚修改它们的值不会影响其他包含着两个值的代码的正确性,而在多线程的情况下作出这样的判断显得尤其困难。

对比下面这段代码:

+(NSUInteger)computeSumOf:(NSUInteger)a plus:(NSUInteger)b
{
 return a + b;
}

这段代码中,a和b的从属显得十分清晰,不再需要去改变实例的状态来调用这个方法,而且不用担心调用这个方法的副作用。

那这个例子和单例又有什么关系呢?事实上,单例就是披着羊皮的全局状态。一个单例可以在任何地方被使用,而且不用清晰地声明从属。程序中的任何模块都可以简单的调用[MySingleton sharedInstance],然后拿到这个单例的访问点,这意味着任何和单例交互时产生的副作用都会有可能影响程序中随机的一段代码,如:

@interface MySingleton : NSObject

+(instancetype)sharedInstance;

-(NSUInteger)badMutableState;
-(void)setBadMutableState:(NSUInteger)badMutableState;

@end

@implementation ConsumerA

-(void)someMethod
{
 if([[MySingleton sharedInstance] badMutableState]){
 //do something...
 }
}

@end

@implementation ConsumerB

-(void)someOtherMethod
{
 [[MySingleton sharedInstance] setBadMutableState:0];
}

在上面的代码中,ConsumerA和ComsumerB是程序中两个完全独立的模块,但是ComsumerB中的方法会影响到ComsumerA中的行为,因为这个状态的改变通过单例传递了过去。

在这段代码,正是因为单例的全局性和状态性,导致了ComsumerA和ComsumerB这两个看起来似乎毫无关系的模块之间隐含的耦合。

对象生命周期

另一个单例的主要问题是它们的生命周期。

举个例子,假设一个app中需要实现能够让用户看到他们的好友列表的功能,每一个好友有自己的头像,同时我们还希望这个app能够下载并缓存这些好友的头像。这时候通过之前学习单例的知识,我们很可能会写出以下的代码:

@interface MyAppCache : NSObject

+(instancetype)sharedCMyAppCache;

-(void)cacheProfileImage:(NSData *)imageData forUserId:(NSString *)userID;
-(NSData *)cachedProfileImageForUserId:(NSString *)userId;

@end

这段代码看起来完全没有问题,运行起来也很好,所以app继续开发,直到有一天,我们决定帮app加入“登出”的功能。突然我们发现,用户数据储存在全局单例中。当用户登出的时候,我们想要把这些数据清除掉,当新用户登入的时候,再为他创建一个新的MyAppCache。

但是问题出在了单例这里,因为单例的定义就是:“创建一次,永久存活”的实例。事实上有很多方法解决上面的问题,我们也许可以在用户登出的时候销毁这个单例:

static MyAppCache *myAppCache;

+(instancetype)sharedMyAppCache
{
 if(!myAppCache)
 {
 myAppCache = [[self alloc] init];
 }
 return myAppCache;
}

+(void)tearDown
{
 myAppCache = nil;
}

上面的代码扭曲了单例这个模式,但是能起到作用。

事实上的确可以使用这个方法来解决这个问题,但是代价太大了。最重要的一点是我们放弃了dispatch_once,而它正是保证了方法调用时候的线程安全,现在所有调用[MyAppCache shareMyAppCache]的代码都会得到同一个变量,着需要清楚使用MyAppCache代码执行的顺序。试想一下当用户在登出的时候碰巧后台调用了这个方法来保存图片。

另一方面,实行这个方法需要确保tearDown这个方法不会在后台任务还没执行完成的时候调用,或者说确保执行tearDown方法的时候后台任务都会被取消。否则另一个新的MyAppCache将会创建,并把陈旧的数据保存进去。

但是由于单例没有明确的owner(因为单例自己管理自己的生命周期),销毁一个单例是非常艰难的。

所以这时你可能会想,“那就不要把MyAppCache做成单例吧!”其实问题在于一个对象的生命周期在项目初期可能没有办法很好的确定,如果假设一个对象的生命周期将会匹配整个程序的生命周期,这将会大大限制了代码的可拓展性,当产品需求改动的时候这将会很痛苦。

所以上面的一切都是为了阐明一个观点:“单例只应该保持全局状态,且该状态的生命周期与程序的生命周期一致”。对于程序中已经存在的单例,需要批判性的审阅。

不利于测试

关于这一部分原文中放到了上一章节中提及,但我认为在软件开发中测试是十分重要的一环,所以单独把这一块的内容另开一个章节,并加入一些个人的见解。

由于单例一直在整个app的生命周期中存活着,甚至在执行测试的时候也一直存活着,这导致了在一个测试或许会影响另一个测试,这是在单元测试中的大忌。
所以有必要在进行单元测试的时候能够有效销毁一个单例,并保持住单例线程安全的特性。但在上文中我提到:

"但是由于单例没有明确的owner(因为单例自己管理自己的生命周期),销毁一个单例是非常艰难的。"

似乎两者在自相矛盾,其实不然,可以选择简化单例,与其拥有各种的单例,不如只拥有一个“真正的” 单例ServiceRegistry,而把其他“潜在的”单例来被ServiceRegistry引用,这样其他单例拥有了一个owner,能够在进行单元测试的时候能够及时对单例进行销毁,保证了单元测试的独立性。

另一方面,ServiceRegistry的存在使得其他“单例”不再是单例,这样在TDD的时候会让之前难以 mock 的单例变得更加简单的 mock 。

结论

我们都知道全局可变状态是不好的,但是在使用单例的时候我们又不经意地把它变成我们讨厌的全局可变状态。
在面向对象编程中,我们需要尽可能减少可变状态的作用域,而单例与这个思想背道而驰,希望在下一次使用单例的时候能够多想一想,考虑是否这个变量真正值得成为一个单例,如果不是,还请使用“依赖注入模式”来代替。

翻译、修改自obj.io

原文链接:Avoiding Singleton Abuse

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • 谈一谈iOS单例模式

    单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源.如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案. 1.书写步骤 1).创建类方法,返回对象实例.以shared  default current开头. 2).创建一个全局变量用来保存对象的引用 3).判断对象是否存在,若不存在,创建对象 2.具体单例模式的几种模式 第一种单例模式 //非线程

  • iOS单例的创建与销毁示例

    单例:单例模式使一个类只有一个实例.单例是在使用过程,保证全局有唯一的一个实例.这样,才能满足统一管理的功能.例如,一个数据库,只需要全局统一的读取,写入操作.不要多个实例去读写.d单例是唯一实例,它不等同于一直伴随这app的生命周期.下面,我会从单例的创建与销毁去分析单例. 单例的创建 单例的创建分为arc与mrc,两种模式下的创建. ARC 下的创建 先定义一个静态的instance. static MyClass _instance; 重写allocWithZone方法.此方法为对象分配空

  • IOS Swift3 四种单例模式详解及实例

    Swift3 单例模式 常见的有这么几种方法 第一种简单到爆的 final class Single: NSObject { static let shared = Single() private override init() {} } final关键字的作用是这个类或方法不希望被继承和重写 第二种 public extension DispatchQueue { private static var onceToken = [String]() public class func once

  • 使用设计模式中的Singleton单例模式来开发iOS应用程序

    单例设计模式确切的说就是一个类只有一个实例,有一个全局的接口来访问这个实例.当第一次载入的时候,它通常使用延时加载的方法创建单一实例. 提示:苹果大量的使用了这种方法.例子:[NSUserDefaults standerUserDefaults], [UIApplication sharedApplication], [UIScreen mainScreen], [NSFileManager defaultManager] 都返回一个单一对象. 你可能想知道你为什么要关心一个类有多个的实例.代码

  • iOS App开发中使用设计模式中的单例模式的实例解析

    一.单例的作用 顾名思义,单例,即是在整个项目中,这个类的对象只能被初始化一次.它的这种特性,可以广泛应用于某些需要全局共享的资源中,比如管理类,引擎类,也可以通过单例来实现传值.UIApplication.NSUserDefaults等都是IOS中的系统单例. 二.单例模式的两种写法 1,常用写法 #import "LGManagerCenter.h" static LGManagerCenter *managerCenter; @implementation LGManagerCe

  • IOS 中两种单例模式的写法实例详解

    iOS的单例模式有两种官方写法,如下: (1)不使用GCD #import "ServiceManager.h" static ServiceManager *defaultManager; @implementation ServiceManager +(ServiceManager *)defaultManager{ if(!defaultManager) defaultManager=[[self allocWithZone:NULL] init]; return default

  • 详解IOS 单例的两种方式

    详解IOS 单例的两种方式 方法一: #pragma mark - #pragma mark sharedSingleton methods //单例函数 static RtDataModel *sharedSingletonManager = nil; + (RtDataModel *)sharedManager { @synchronized(self) { if (sharedSingletonManager == nil) { sharedSingletonManager = [[sel

  • iOS开发教程之单例使用问题详析

    导语 单例(Singletons),是Cocoa的核心模式之一.在iOS上,单例十分常见,比如:UIApplication,NSFileManager等等.虽然它们用起来十分方便,但实际上它们有许多问题需要注意.所以在你下次自动补全dispatch_once代码片段的时候,想一下这样会导致什么后果. 什么是单例 在<设计模式>一书中给出了单例的定义: 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点. 单例模式提供了一个访问点,供客户类为共享资源生成唯一实例,并通过它来对共享资源

  • 通过spring注解开发,简单测试单例和多例区别

    目录 通过spring注解开发,测试单例和多例区别 1.注解和配置两种用法形式 2.在spring框架中,scope作用域默认是单例的 3.实例 (1)多例: (2)单例(注解版) Spring中单例和多例的理解 1.什么是单例和多例 2.Spring中的单例与多例 单例bean与多例(原型)bean的区别: 3.单例的优势与劣势 优势: 劣势: 4.spring单例模式与线程安全: 如何解决线程安全问题? 5.单例如何变多例 通过spring注解开发,测试单例和多例区别 1.注解和配置两种用法

  • IOS开发之字典转字符串的实例详解

    IOS开发之字典转字符串的实例详解 在实际的开发需求时,有时候我们需要对某些对象进行打包,最后拼接到参数中 例如,我们把所有的参数字典打包为一个 字符串拼接到参数中 思路:利用系统系统JSON序列化类即可,NSData作为中间桥梁 //1.字典转换为字符串(JSON格式),利用 NSData作为桥梁; NSDictionary *dic = @{@"name":@"Lisi",@"sex":@"m",@"tel&qu

  • IOS开发中NSURL的基本操作及用法详解

    NSURL其实就是我们在浏览器上看到的网站地址,这不就是一个字符串么,为什么还要在写一个NSURL呢,主要是因为网站地址的字符串都比较复杂,包括很多请求参数,这样在请求过程中需要解析出来每个部门,所以封装一个NSURL,操作很方便. 1.URL URL是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址.互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它. URL可能包含远程服务器上的资源的位置,本地磁盘上的文件的路径,甚

  • java 设计模式之单例的实例详解

    java 设计模式之单例的实例详解 设计模式思想 什么是设计模式:我作为初学者,今天第一次正式学习设计模式,我觉得对与理解什么是设计模式很重要,那么什么是设计模式呢? 设计模式:解决问题的一种行之有效的思想. 设计模式:用于解决特定环境下.重复出现的特定问题的解决方案 我的理解是前人在软件设计的时候碰到了一类问题,他们总结出了一套行之有效,并且经过验证的解决方案. 设计模式的优点: 1.设计模式都是一些相对优秀的解决方案,很多问题都是典型的.有代表性的问题,学习设计模式,我们就不用自己从头来解决

  • JAVA 静态的单例的实例详解

    JAVA  静态的单例的实例详解 实现代码: public class Printer { private Printer(){ } public static Printer newInstance(){ return CreatePrinter.mPrinter; } private static class CreatePrinter{ private final static Printer mPrinter = new Printer(); } } 因为静态的单例对象没有作为类的成员变

  • C++单例类模板详解

    单例类 描述 指在整个系统生命期中,一个类最多只能有一个实例(instance)存在,使得该实例的唯一性(实例是指一个对象指针)  , 比如:统计在线人数 在单例类里,又分为了懒汉式和饿汉式,它们的区别在于创建实例的时间不同: 懒汉式 : 指代码运行后,实例并不存在,只有当需要时,才去创建实例(适用于单线程) 饿汉式 : 指代码一运行,实例已经存在,当时需要时,直接去调用即可(适用于多线程) 用法 将构造函数的访问属性设置为private, 提供一个GetInstance()静态成员函数,只能供

  • Java之单例设计模式示例详解

    单例设计模式 保证一个类在内存中只能有一个对象. 思路: 1)如果其他程序能够随意用 new 创建该类对象,那么就无法控制个数.因此,不让其他程序用 new 创建该类的对象. 2)既然不让其他程序 new 该类对象,那么该类在自己内部就要创建一个对象,否则该类就永远无法创建对象了. 3)该类将创建的对象对外(整个系统)提供,让其他程序获取并使用. 饿汉式: 一上来我就把对象给你 new 好了,你来了直接就可以拿去"吃"了 懒汉式 (要是有人问单例的延迟加载方式指的就是这种方式) 一开始

  • JAVA  静态的单例的实例详解

    JAVA  静态的单例的实例详解 实现代码: public class Printer { private Printer(){ } public static Printer newInstance(){ return CreatePrinter.mPrinter; } private static class CreatePrinter{ private final static Printer mPrinter = new Printer(); } } 因为静态的单例对象没有作为类的成员变

  • iOS开发多线程下全局变量赋值崩溃原理详解

    目录 问题 Demo 崩溃原因 崩溃路径 验证方式 其它测试 问题 Demo 在多线程下同时给全局变量赋值时会发生崩溃: static NSObject *_instance; - (void)foo { _instance = [[NSObject alloc] init]; } 崩溃原因 如下为源码的汇编代码: Demo-iOS`-[ViewController foo]: 0x104e4e088 <+0>: stp x29, x30, [sp, #-0x10]! 0x104e4e08c

随机推荐