映各 发表于 前天 09:42

设计模式之单例模式

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

1. 单例模式的定义

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

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

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

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

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

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

1. 实现单例类的思路

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

懒汉模式:当使用这个单例对象的时候,才创建对象,就是 _instance 的懒加载形式。由于移动设备内存有限,所以这种方式最适合。
饿汉模式:当类第一次加载的时候,就创建单例对象,并保存在 _instance 中。由于第一次加载就创建,内存从程序开始运行的时候就分配了,不适合移动设备。
3. 单例基本形式(懒汉模式)

@interface XBLoadTool : NSObject
// 给外界快速创建单例对象使用
+ (instancetype)sharedLoadTool;
@end

#import "XBLoadTool.h"

// 定义全局静态变量,用来存储创建好的单例对象,当外界需要时,返回
static id _instance;

@implementation XBLoadTool

// 给外界快速创建单例对象使用
+ (instancetype)sharedLoadTool {
    if (_instance == nil) {
        // 避免出现多个线程同时创建_instance,加锁
        @synchronized (self) {
            // 使用懒加载,确保_instance只创建一次
            if (_instance == nil) {
                _instance = [ init];
            }
        }
    }
    return _instance;
}

// 重写allocWithZone:方法---内存与sharedLoadTool方法体基本相同
+ (instancetype)allocWithZone:(NSZone *)zone {
    // 避免每次线程过来都加锁,首先判断一次,如果为空才会继续加锁并创建对象
    if(_instance == nil) {
        // 避免出现多个线程同时创建_instance,加锁
        @synchronized(self) {
            // 使用懒加载,确保_instance只创建一次
            if(_instance == nil) {
                //调用父类方法,分配空间
                _instance = ;
            }
        }
    }
    return _instance;
}

// 重写copyWithZone:方法,避免实例对象的copy操作导致创建新的对象
- (instancetype)copyWithZone:(NSZone *)zone {
    // 由于是对象方法,说明可能存在_instance对象,直接返回即可
    return _instance;
}

@end为什么全局变量要使用 static 修饰?
static 修饰局部变量:

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

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

考虑到线程安全,苹果官方推荐开发者使用 dispatch_once_t 来创建单例类。上面实例中,在 allocWithZone 方法和 sharedLoadTool 中,每次需要判断是否为空,然后加锁,其目的是为了保证 [init] 和 代码只执行一次,那么可以使用 GCD 的一次性代码解决,另外,GCD 一次性代码是线程安全的,所以不需要我们自己来处理加锁问题。
// 创建单例类方法,供全局调用 - retutn type instancetype
+ (instancetype)shareOnceClass {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _onceClass = [ init];
    });
    return _onceClass;
}

// 修改 allocWithZone 方法
+ (instancetype)allocWithZone:(NSZone *)zone{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _onceClass = [super allocWithZone:zone
];
    });
    return _onceClass;
}

// 重写 copyWithZone:方法,避免实例对象的 copy 操作导致创建新的对象
-(instancetype)copyWithZone:(NSZone *)zone
{
    //由于是对象方法,说明可能存在_onceClass对象,直接返回即可
    return _onceClass;
}由于移动端特性,我们在开发过程中多用 GDG(懒汉模式)来创建单例。对于饿汉模式在第五大点有提到。
四.单例模式的优缺点

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

1. 初始化过程解析

重写单例类的 alloc->allocWithZone 方法,确保这个单例类只被初始化一次。
在 viewDidLoad 方法中调用单例类的 alloc 和 init 方法:[ init];
此时只是报黄点,但是并没有报错,Run 程序也可以成功,这样的话,就不符合我们最开始使用单例模式的初衷来,这个类也可以随便初始化类,为什么呢?因为我们并没有获取 OneTimeClass 类中的使用实例;
因此可以重写 alloc 方法的处理可以采用断言或者系统为开发者提供的 NSException 类来告诉其他的同事这个类是单例类,不能多次初始化。
// 断言
+ (instancetype)alloc {
    NSCAssert(!_onceClass, @"单例XBOnceClass只能被初始化一次");
    return ;
}

//NSException
+ (instancetype)alloc {
   //如果已经初始化了
    if (_onceClass) {
      NSException *exception = ;
      ;
   }
  return ;
}但是,如果我们的程序直接就崩溃了,这样的做法与开发者开发 APP 的初衷是不是又相悖了,作为一个程序员的目的要给用户一个交互友好的 APP,而不是一点小问题就崩溃。对于这种情况,可以用到 NSObect 类提供的 load 方法和 initialize 方法来控制,
这两个方法的调用时机,load 方法:当程序开始运行的时候,所有类都会加载到内存中(不管这个类有没有使用),此时就会调用 load 方法,如果想某个操作在程序运行的过程中只执行一次,那么这个操作就可以放到 load 中,且在 main 函数调用之前调用,基于以上特点饿汉模式的单例创建就是放在 load 方法中; initialize 方法是当类第一次被使用的时候调用(比如调用类的方法),在 main 函数调用之后调用,如果子类没有重写该方法,那么父类的 initialize 方法可能会被执行多次,所以饿汉模式不能使用这种方法;
这样的话,饿汉模式下,如果我在单例类的 load 方法初始化这个类,是不是就保证了这个类在整个程序中调用一次呢?
这样就可以保证 sharedMusicTool 方法是最早调用的。同时,再次对 alloc 方法修改,无论在何时调用 instance 已经初始化了,如果再次调用 alloc 可直接返回_instance 实例。
@interface XBMusicTool : NSObject

//提供外界访问的方法
+(instancetype)sharedMusicTool;

@end

#import "XBMusicTool.h"

// 定义静态全局变量
static id _instance;

@implementation XBMusicTool

// 实现方法
+ (instancetype)sharedMusicTool {
    return _instance;
}

// 重写load方法
+ (void)load {
    // 不需要线程安全,类加载的时候线程还没开始呢
    _instance = [init];
}

// 重写allocWithZone方法
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    if(_instance == nil) {
        _instance = ;
    }
    return _instance;
}

// 重写copyWithZone:方法,避免实例对象的copy操作导致创建新的对象
-(instancetype)copyWithZone:(NSZone *)zone {
    // 由于是对象方法,说明可能存在_instance对象,直接返回即可
    return _instance;
}

@end最后在 ViewController 中打印调用 XBMusicTool 的 sharedMusicTool 和 alloc 方法,可以看到 Log 出来的内存地址是相同的,这就说明此时我的 XBMusicTool 类就只初始化了一次。
2. 直接禁止方法的使用

直接禁用方法,禁止调用这几个方法,否则就报错,编译不过,不建议使用。
-(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

来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除
页: [1]
查看完整版本: 设计模式之单例模式