[iOS] 单例模式的深究
文章目录
- 前言
- 一、什么是单例模式
- 二、单例模式的优缺点
- 优点
- 缺点
- 三、模式介绍
- 1.懒汉模式(GCD & 互斥锁)
- GCD 写法
- 互斥锁写法(双重检查锁)
- 2.饿汉模式
- 总结
- 懒汉式 + 互斥锁(Mutex)
- **懒汉式 + GCD (dispatch_once) **
- 饿汉式
前言
这篇博客是对单例模式的一个深入了解,之前的对单例模式的学习太过浅薄了,所以撰写这篇博客来去深入对单例的理解。
一、什么是单例模式
单例模式 是一种常见的设计模式,核心思想是: 保证一个类在整个程序运行期间,只有唯一一个实例,并且提供一个全局访问点。它可以做到大大减少内存的使用,防止一个实例被重复创建从而占用内存空间,他在共享资源和对象的情况下非常有用。
下面给出一些 OC 中常见的单例
- NSUserDefaults → 用户偏好设置
- UIApplication → App 的入口对象
- NSFileManager → 文件管理器
- NSNotificationCenter → 通知中心
二、单例模式的优缺点
优点
- 使整个应用内只有一个实例,避免数据冲突。
- 提供了一个全局访问的点,使用方便,不需要每次都 alloc/init。
- 减少了内存开销,节省资源。
- 数据的一致性。
缺点
-
因为是全局的状态,增加了耦合,相当于一个全局变量,如果被滥用,会导致模块之间的依赖性增强,代码的维护度降低。
-
单例对象的生命周期和应用的周期一样长,很难被替换。
-
在测试的时候很难被替换掉。
-
使用单例的类,不显式传入依赖,而是全局取对象,导致代码逻辑不透明。
-
而且他因为生命周期太长导致一直持有大量资源,内存占用过高。
三、模式介绍
1.懒汉模式(GCD & 互斥锁)
在这里我给出两个懒汉模式
要知道在写任何单例模式的时候都需要重写三个方法因为我们要让alloc和copy还有mutablecopy这三个方法返回的单例保持一致,所以我们要重写如下几个方法。
+ (instancetype)allocWithZone:(struct _NSZone *)zone
- (id)copyWithZone:(NSZone *)zone
-(id)mutableCopyWithZone:(NSZone *)zone
GCD 写法
这个写法是苹果公司最推荐的写法,下面我会给出代码以及解释为什么要推荐这种写法。
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface LazyGcdSingleton : NSObject<NSCopying, NSMutableCopying>
+ (instancetype) sharedInstance;
@endNS_ASSUME_NONNULL_END
//懒汉式GCD写法
#import "LazyGcdSingleton.h"
static id _instance = nil;
@implementation LazyGcdSingleton
+ (instancetype)sharedInstance {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{_instance = [[super allocWithZone:NULL] init];});return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {return [LazyGcdSingleton sharedInstance];
}
- (id)copyWithZone:(NSZone *)zone {return [LazyGcdSingleton sharedInstance];
}
- (id) mutableCopyWithZone:(NSZone *)zone {return [LazyGcdSingleton sharedInstance];
}
@end
- static dispatch_once_t onceToken;
-
定义了一个静态变量 onceToken,它的作用就是 保证下面的 block 只会执行一次。
-
dispatch_once_t 是苹果专门设计的类型,用来做单例的线程安全初始化。
- dispatch_once(&onceToken, ^{ … });
- dispatch_once 内部帮你做了 加锁 + 判断是否执行过 的工作。
- 无论多少线程同时调用 sharedInstance,block 里的代码只会被执行一次。
- 所以这里的 _instance = [[super allocWithZone:NULL] init]; 只会执行一次,保证单例唯一性。
- _instance = [[super allocWithZone:NULL] init];
- 真正创建对象的地方。
- 用 super allocWithZone:NULL 是为了绕过 allocWithZone: 的重写(因为我们在类里也重写了它,防止外部直接用 alloc 创建新对象)。
- 这样写就能保证只创建 一个实例对象。
推荐这个写法的最根本的原因就是 dispatch_once 他会帮助你加锁和判断是否执行过的工作。
互斥锁写法(双重检查锁)
这里我给出代码以及会给出一些读者可能的疑问的解答
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface LazyLockSingleton : NSObject<NSCopying, NSMutableCopying>
+ (instancetype) sharedInstance;
@property (nonatomic, strong) NSString *info;@endNS_ASSUME_NONNULL_END
#import "LazyLockSingleton.h"
static id _instance = nil;
@implementation LazyLockSingleton
+ (instancetype)sharedInstance {if (_instance == nil) {@synchronized (self) {if (_instance == nil) {_instance = [[super alloc] init];}}}return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {if (_instance == nil) {@synchronized (self) {if (_instance == nil) {_instance = [super allocWithZone:zone];}}}return _instance;
}
- (id) copyWithZone:(NSZone *)zone {return [LazyLockSingleton sharedInstance];
}
- (id) mutableCopyWithZone:(NSZone *)zone {return [LazyLockSingleton sharedInstance];
}
@end
这里可能会有这样几个问题会有疑惑
1.是否可以这样创建单例
+ (instancetype)sharedInstance {if (_instance == nil) {@synchronized (self) {_instance = [[super alloc] init];}}return _instance;
}
实则不行,我们只是锁住了对象的创建,如果两个线程同时进入 if,那么就会产生两个对象。
2.为什么要用 static
如果不用 static,其他的类中可以使用 extern 来拿到这个单例
extern id instance;
instance = nil;
如果其他类中对单例进行如下操作,那么单例就会被重新创建,我们原本的单例对象中的内容就被销毁了。
2.饿汉模式
饿汉模式是一种单例模式实现方式,它在类加载的时候就创建好唯一的实例,保证整个程序运行期间全局只有一个对象。
它有几个非常奇妙的特点
- 线程安全
- 因为实例在类加载阶段就已经创建完成,不存在多线程同时创建的问题。
- 立即初始化
- 无论程序是否会用到这个对象,它都会在类加载时创建。
- 简单易实现
- 只需一个静态变量初始化即可,不需要加锁或 dispatch_once。
缺点
- 如果实例比较大或创建开销高,而程序又不一定会用到,可能浪费内存和启动时间。
下面给出代码
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface EagerSingleton : NSObject<NSCopying, NSMutableCopying>
+ (instancetype) sharedInstance;
@property (nonatomic, strong) NSString *info;
@endNS_ASSUME_NONNULL_END
//饿汉式写法
#import "EagerSingleton.h"
static id _instance;
@implementation EagerSingleton
+ (void)load {_instance = [[self alloc] init];
}
+ (instancetype)sharedInstance {return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {if (!_instance) {_instance = [super allocWithZone:zone];}return _instance;
}
- (id) copyWithZone:(NSZone *)zone {return _instance;
}
- (id) mutableCopyWithZone:(NSZone *)zone {return _instance;
}
@end
在这段代码中我个人认为
+ (void)load {_instance = [[self alloc] init];
}
这段代码完全体现了饿汉和懒汉的区别,它直接写了一个类方法来去创建了一个后面需要用到的对象。
总结
其实有一个很形象的说法来去阐述饿汉和懒汉的区别就是饿汉就是直接吃饱再去干活,而懒汉就是等到需要吃的时候再去吃和干活,
下面我来总结一下懒汉(GCD & 互斥锁)和饿汉
懒汉式 + 互斥锁(Mutex)
-
核心逻辑:使用 @synchronized(self) 对实例创建加锁
-
创建时机:第一次调用 sharedInstance 时才创建
-
线程安全: 保证线程安全
-
优点:
- 按需创建(懒加载)
- 保证全局唯一
-
缺点:
每次访问都加锁,性能较低
-
使用场景:小型项目,理解单例原理
**懒汉式 + GCD (dispatch_once) **
- 核心逻辑:使用 dispatch_once 保证 block 只执行一次
- 创建时机:第一次调用 sharedInstance 时创建
- 线程安全: 系统保证线程安全
- 优点:
- 按需创建(懒加载)
- 高效,只有第一次创建加锁
- 代码简洁、官方推荐
- 缺点:几乎无明显缺点
- 使用场景:iOS / macOS 官方标准单例实现
饿汉式
-
核心逻辑:在类加载阶段就创建静态实例
-
创建时机:类加载时(程序启动)
-
线程安全: 天然线程安全
-
优点:
- 实现简单
- 天然线程安全
-
缺点:
程序启动时就创建对象,如果对象大或未使用,可能浪费资源
-
使用场景:必须全局存在的管理类,如配置管理器、日志管理器