找回密码
 立即注册
首页 业界区 安全 设计模式之单例模式

设计模式之单例模式

颖顿庐 10 小时前
认真对待每时、每刻每一件事,把握当下、立即去做。
单例模式,由于其简单好用容易理解、同时在出问题时也容易定位的特点,在开发中经常用到的一个设计模式。
一.什么是单例模式

1. 单例模式的定义

简单的来说,一个单例类,在整个程序中只有一个实例,并且提供一个类方法供全局调用,在编译时初始化这个类,然后一直保存在内存中,到程序(APP)退出时系统自动释放这部分内存。
2. 系统单例类了解

UIApplication(应用程序实例类)
NSNotificationCenter(消息中心类)
NSFileManager(文件管理类)
NSUserDefaults(应用程序设置)
NSURLCache(请求缓存类)
NSHTTPCookiesStorage(应用程序cookies池)
3. 在那些地方会常用到单例类

一般在应用程序中,经常需要调用的类,比如工具类,公共跳转类等等,都建议使用单例模式。
二.单例模式的生命周期

1. 单例实例在存储器中的位置

程序中不同变量在手机存储器中的存储位置详情见“内存管理”专题。
在程序中,一个单例类在只能初始化一次,为了保证在使用时始终都是存在的,所以单例是在存储器的全局区域。在编译时分配内存,只要程序还在运行就会一直占用内存,在 APP 结束后由系统释放这部分内存。
2. 多次初始化单例类会发生什么?

下面代码我们在工程中初始化一次 UIApplication。最终运行的结果如下,程序直接崩溃,由此可以确定,一个单例类只能初始化一次。
  1. [[UIApplication alloc] init];
  2. Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'There can only be one UIApplication instance.'
复制代码
三.实现单例类

1. 实现单例类的思路

只能分配一次内存(初始化一次),因此要拦截 alloc 方法。alloc 方法的底层是 allocWithZone 方法。
每个类只有一个对象,需要一个全局静态变量来存储这个对象。
需要考虑线程安全。
2. 单例的两种模式

懒汉模式:当使用这个单例对象的时候,才创建对象,就是 _instance 的懒加载形式。由于移动设备内存有限,所以这种方式最适合。
饿汉模式:当类第一次加载的时候,就创建单例对象,并保存在 _instance 中。由于第一次加载就创建,内存从程序开始运行的时候就分配了,不适合移动设备。
3. 单例基本形式(懒汉模式)
  1. @interface XBLoadTool : NSObject
  2. // 给外界快速创建单例对象使用
  3. + (instancetype)sharedLoadTool;
  4. @end
  5.   
  6. #import "XBLoadTool.h"
  7. // 定义全局静态变量,用来存储创建好的单例对象,当外界需要时,返回
  8. static id _instance;
  9. @implementation XBLoadTool
  10. // 给外界快速创建单例对象使用
  11. + (instancetype)sharedLoadTool {
  12.     if (_instance == nil) {
  13.         // 避免出现多个线程同时创建_instance,加锁
  14.         @synchronized (self) {
  15.             // 使用懒加载,确保_instance只创建一次
  16.             if (_instance == nil) {
  17.                 _instance = [[self alloc] init];
  18.             }
  19.         }
  20.     }
  21.     return _instance;
  22. }
  23. // 重写allocWithZone:方法---内存与sharedLoadTool方法体基本相同
  24. + (instancetype)allocWithZone:(NSZone *)zone {
  25.     // 避免每次线程过来都加锁,首先判断一次,如果为空才会继续加锁并创建对象
  26.     if(_instance == nil) {
  27.         // 避免出现多个线程同时创建_instance,加锁
  28.         @synchronized(self) {
  29.             // 使用懒加载,确保_instance只创建一次
  30.             if(_instance == nil) {
  31.                 //调用父类方法,分配空间
  32.                 _instance = [super allocWithZone:zone];
  33.             }
  34.         }
  35.     }
  36.     return _instance;
  37. }
  38. // 重写copyWithZone:方法,避免实例对象的copy操作导致创建新的对象
  39. - (instancetype)copyWithZone:(NSZone *)zone {
  40.     // 由于是对象方法,说明可能存在_instance对象,直接返回即可
  41.     return _instance;
  42. }
  43. @end
复制代码
为什么全局变量要使用 static 修饰?
static 修饰局部变量:

  • 其生命周期与全局变量相同,直到程序结束,只有一份内存空间。
  • 内存空间:仅有一份,多次调用函数时保留上次的值。
  • 作用域不变,仅限定义它的函数或代码块内,与未加static的局部变量作用域一致。
static 修饰全局变量:

  • 内存空间:只有一份内存空间,全局变量本身只有一份,static 修饰后不改变此特性。
  • 全局变量可以在其他文件中,通过 extern  id _instance 来声明,然后直接在其他文件中调用。用 static 会将全局变量的链接属性从 external 改为 internal,使其仅在当前文件可见,其他文件无法通过 extern 引用,任何方式都无法跨文件访问。
加锁且懒加载的原理:懒加载是为了,确保整个类只有一个 instance。加锁:多线程中,可能多个线程都发现当前的 _instance==nil,那么就会同时创建对象,不符合单例的原则,所以加锁。但是加锁容易引起效率降低,不能每次线程过来就加锁,所以在加锁之前首先判断一次是否为空,不为空根本不需要创建,直接返回。为空则说明可能需要创建对象,那么再加锁。
3. GCD(dispatch_once_t)创建单例(懒汉模式)

考虑到线程安全,苹果官方推荐开发者使用 dispatch_once_t 来创建单例类。上面实例中,在 allocWithZone 方法和 sharedLoadTool 中,每次需要判断是否为空,然后加锁,其目的是为了保证 [[self alloc]init] 和 [super allocWithZone:zone] 代码只执行一次,那么可以使用 GCD 的一次性代码解决,另外,GCD 一次性代码是线程安全的,所以不需要我们自己来处理加锁问题。
  1. // 创建单例类方法,供全局调用 - retutn type instancetype
  2. + (instancetype)shareOnceClass {
  3.     static dispatch_once_t onceToken;
  4.     dispatch_once(&onceToken, ^{
  5.         _onceClass = [[XBOnceClass alloc] init];
  6.     });
  7.     return _onceClass;
  8. }
  9. // 修改 allocWithZone 方法
  10. + (instancetype)allocWithZone:(NSZone *)zone{
  11.     static dispatch_once_t onceToken;
  12.     dispatch_once(&onceToken, ^{
  13.         _onceClass = [super allocWithZone:zone
  14. ];
  15.     });
  16.     return _onceClass;
  17. }
  18. // 重写 copyWithZone:方法,避免实例对象的 copy 操作导致创建新的对象
  19. -(instancetype)copyWithZone:(NSZone *)zone
  20. {
  21.     //由于是对象方法,说明可能存在_onceClass对象,直接返回即可
  22.     return _onceClass;
  23. }
复制代码
由于移动端特性,我们在开发过程中多用 GDG(懒汉模式)来创建单例。对于饿汉模式在第五大点有提到。
四.单例模式的优缺点

优点:
1)在整个程序中只会实例化一次,所以在程序如果出了问题,可以快速的定位问题所在;
2)由于在整个程序中只存在一个对象,节省了系统内存资源,提高了程序的运行效率;
缺点:
1)不能被继承,不能有子类;
2)不易被重写或扩展(可以使用分类);
3)同时,由于单例对象只要程序在运行中就会一直占用系统内存,该对象在闲置时并不能销毁,在闲置时也消耗了系统内存资源;
五.单例模式过程详解

1. 初始化过程解析

重写单例类的 alloc->allocWithZone 方法,确保这个单例类只被初始化一次。
在 viewDidLoad 方法中调用单例类的 alloc 和 init 方法:[[XBOnceClass alloc] init];
此时只是报黄点,但是并没有报错,Run 程序也可以成功,这样的话,就不符合我们最开始使用单例模式的初衷来,这个类也可以随便初始化类,为什么呢?因为我们并没有获取 OneTimeClass 类中的使用实例;
因此可以重写 alloc 方法的处理可以采用断言或者系统为开发者提供的 NSException 类来告诉其他的同事这个类是单例类,不能多次初始化。
  1. // 断言
  2. + (instancetype)alloc {
  3.     NSCAssert(!_onceClass, @"单例XBOnceClass只能被初始化一次");
  4.     return [super alloc];
  5. }
  6. //NSException
  7. + (instancetype)alloc {
  8.    //如果已经初始化了
  9.     if (_onceClass) {
  10.       NSException *exception = [NSException exceptionWithName:@"提示" reason:@"XBOnceClass类只能初始化一次" userInfo:nil];
  11.       [exception raise];
  12.    }
  13.   return [super alloc];
  14. }
复制代码
但是,如果我们的程序直接就崩溃了,这样的做法与开发者开发 APP 的初衷是不是又相悖了,作为一个程序员的目的要给用户一个交互友好的 APP,而不是一点小问题就崩溃。对于这种情况,可以用到 NSObect 类提供的 load 方法和 initialize 方法来控制,
这两个方法的调用时机,load 方法:当程序开始运行的时候,所有类都会加载到内存中(不管这个类有没有使用),此时就会调用 load 方法,如果想某个操作在程序运行的过程中只执行一次,那么这个操作就可以放到 load 中,且在 main 函数调用之前调用,基于以上特点饿汉模式的单例创建就是放在 load 方法中; initialize 方法是当类第一次被使用的时候调用(比如调用类的方法),在 main 函数调用之后调用,如果子类没有重写该方法,那么父类的 initialize 方法可能会被执行多次,所以饿汉模式不能使用这种方法;
这样的话,饿汉模式下,如果我在单例类的 load 方法初始化这个类,是不是就保证了这个类在整个程序中调用一次呢?
这样就可以保证 sharedMusicTool 方法是最早调用的。同时,再次对 alloc 方法修改,无论在何时调用 instance 已经初始化了,如果再次调用 alloc 可直接返回_instance 实例。
  1. @interface XBMusicTool : NSObject
  2. //提供外界访问的方法
  3. +(instancetype)sharedMusicTool;
  4. @end
  5. #import "XBMusicTool.h"
  6. // 定义静态全局变量
  7. static id _instance;
  8. @implementation XBMusicTool
  9. // 实现方法
  10. + (instancetype)sharedMusicTool {
  11.     return _instance;
  12. }
  13. // 重写load方法
  14. + (void)load {
  15.     // 不需要线程安全,类加载的时候线程还没开始呢
  16.     _instance = [[self alloc]init];
  17. }
  18. // 重写allocWithZone方法
  19. + (instancetype)allocWithZone:(struct _NSZone *)zone {
  20.     if(_instance == nil) {
  21.         _instance = [super allocWithZone:zone];
  22.     }
  23.     return _instance;
  24. }
  25. // 重写copyWithZone:方法,避免实例对象的copy操作导致创建新的对象
  26. -(instancetype)copyWithZone:(NSZone *)zone {
  27.     // 由于是对象方法,说明可能存在_instance对象,直接返回即可
  28.     return _instance;
  29. }
  30. @end
复制代码
最后在 ViewController 中打印调用 XBMusicTool 的 sharedMusicTool 和 alloc 方法,可以看到 Log 出来的内存地址是相同的,这就说明此时我的 XBMusicTool 类就只初始化了一次。
2. 直接禁止方法的使用

直接禁用方法,禁止调用这几个方法,否则就报错,编译不过,不建议使用。
  1. -(instancetype) copy __attribute__((unavailable("OneTimeClass类只能初始化一次")));
复制代码
六.常见问题和学习

1. 如果单例的静态变量被置为 nil 了,是否内存会得到释放?

https://blog.csdn.net/jhcBoKe/article/details/108097693
https://www.jianshu.com/p/5c0d002a0aad
https://www.cnblogs.com/dins/p/ios-singleton.html

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册