1. 最终推荐写法
1.1 OC
SingleObject.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SingleObject : NSObject
+ (instancetype)sharedInstance;
@end
NS_ASSUME_NONNULL_END
SingleObject.m
#import "SingleObject.h"
static SingleObject *_singleInstance = nil;
@implementation SingleObject
+ (instancetype)sharedInstance
{
return [[self alloc] init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_singleInstance = [super allocWithZone:zone];
});
return _singleInstance;
}
- (instancetype)init
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_singleInstance = [super init];
if (_singleInstance) {
// 在这里初始化self的属性和方法
}
});
return _singleInstance;
}
/*
#pragma mark - 如果遵循了 NSCopying / NSMutableCopying 协议
- (id)copyWithZone:(nullable NSZone *)zone
{
return _singleInstance;
}
- (id)mutableCopyWithZone:(nullable NSZone *)zone
{
return _singleInstance;
}
*/
@end
1.2 Swift
摘自苹果文档.
class Singleton {
static let sharedInstance = Singleton()
}
如果需要做一些初始化操作, 使用下面:
class Singleton {
static let sharedInstance: Singleton = {
let instance = Singleton()
// setup code
return instance
}()
}
2. 何为单例?
2.1 单例概念
苹果文档
A singleton class returns the same instance no matter how many times an application requests it. A typical class permits callers to create as many instances of the class as they want, whereas with a singleton class, there can be only one instance of the class per process. A singleton object provides a global point of access to the resources of its class.
翻译过来就是:
- 在App运行期间, 单例类有且仅有一个实例对象;
- 这个单例对象是可以全局访问的.
2.2 几个官方单例
- NSFileManager
- NSWorkspace
- UIApplication
- UIAccelerometer (Deprecated)
按照惯例, 返回单例类实例对象的方法是一种工厂方法, 约定方法名称这样命名:sharedClassType. 比如sharedFileManager, sharedWorkspace, sharedApplication等等.
官方样例: 无
😂😂😂
2.3 单例原理
苹果文档
The class lazily creates its sole instance the first time it is requested and thereafter ensures that no other instance can be created.
五星翻译: 在单例类第一次创建的时候稍微做些处理, 使这个类无法再创建其他实例对象.
简单来说, 就是防止一个单例类被多次创建, 或者说这个单例类的创建方法仅执行一次.
结论是:
让单例类的创建实例方法只执行一次.
让单例类的创建实例方法只执行一次.
让单例类的创建实例方法只执行一次.
接下来我们从对象的创建入手. 请看下小节.
3. 对象的创建
对象的创建分两步: 分配内存和初始化.
常规操作如下:
TheClass *newObject = [[TheClass alloc] init];
分配内存
为对象分配内存有两种方法: alloc 和 allocWithZone:
其实, 使用alloc最终还是会调用allocWithZone:方法.
allocWithZone:这个方法苹果不建议我们直接使用, 但是这个方法在OC中没有被遗弃, 它的存在是历史遗留问题:
This method exists for historical reasons; memory zones are no longer used by Objective-C.
所以, 我们分配内存时应该使用alloc而不是allocWithZone:方法.
但是, 不排除个别人使用allocWithZone:去分配内存的情况. 所以, 后面我们讨论单例的写法时, 还是要考虑这种情况. 这里先埋下伏笔.
初始化
初始化处在创建对象阶段, 该阶段通过将对象的实例变量设置为合理的初始值, 还可以分配和准备对象所需的其他全局资源, 才使得该对象可用.
按照约定, 初始化方法始终以init开头. 该方法返回一个动态类型的对象(id), 或者, 如果初始化失败, 则返回nil.
如果某个类实现了初始化方法, 则第一步应调用其父类的初始化程序. 比如:
- (instancetype)init
{
self = [super init];
if (self) {
// 初始化self的属性和方法
}
return self;
}
此要求可确保从根对象开始在继承链中对对象进行一系列初始化.
工厂方法
工厂方法是一种类方法, 其将分配内存alloc和初始化init结合在一起, 并返回一个自动释放的类实例.
例如:
+ (instancetype)stringWithString:(NSString *)string;
+ (NSNumber *)numberWithInt:(int)value;
我们单例类的获取实例方法, 就是使用工厂方法返回的, 形式如sharedClassType.
new
我们常常看到一个对象的创建方法如下:
NSString *string = [NSString new];
在苹果文档中:
This method is a combination of alloc and init.
其实, new也是工厂方法, new = alloc + init.
4. 单例写法的讨论过程
创建一个SingleObject类, 继承于NSObject. 给这个SingleObject写一个工厂方法用于返回类的实例对象.
SingleObject.h
@interface SingleObject : NSObject
+ (instancetype)sharedInstance;
@end
SingleObject.m
+ (instancetype)sharedInstance {
return [[self alloc] init];
}
OK, 雏形有了, 现在它能返回一个实例. 但仅是这样肯定是不行的, 因为每次调用sharedObject都会重新创建一个实例.
之前说过, 单例的原理是让单例类的创建实例方法只执行一次. 而创建方法分两步: alloc和init. 所以, 创建单例, 我们需做到以下两点(tag=10001):
- alloc只调用一次
- init只调用一次
可是alloc和init方法都是外部可以访问的, 我们不能控制别人使用的次数. 比如外部可以调用多次:
SingleObject *obj1 = [[SingleObject alloc] init];
SingleObject *obj2 = [[SingleObject alloc] init];
...
我们在对象的创建小节里讨论过, 如果某个类实现了初始化方法, 则第一步应调用其父类的初始化程序. 也就是说, 虽然该类的init无法控制, 但是通过重写init方法, 它的父类init是可控的. 所以, tag=10001那两点应该改写表述成如下两点(tag=10086):
- [super alloc]只调用一次
- [super init]只调用一次
问题又来了, 我们之前不是说过分配内存有两种方法吗? 假如创建的时候不是用alloc而是用allocWithZone:方法, 那我们又得干瞪眼了. 好在alloc最终都是调用allocWithZone:方法的, 所以tag=10086这两点最终应修改成:
- [super allocWithZone:]只调用一次
- [super init]只调用一次
这两点需求算是成熟可以告一段落了, 接下来讨论怎样让一段代码只执行一次呢?
苹果给我们提供了dispatch_once这个方法:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 不管调用多少次dispatch_once, 这里的代码只会执行一次
});
关于dispatch_once这里不打算详解, 我们只需要知道它的作用以及它是线程安全的.
我们就用这个方法去实现那两点需求:
#import "SingleObject.h"
static SingleObject *_singleInstance = nil;
@implementation SingleObject
+ (instancetype)sharedInstance
{
return [[self alloc] init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_singleInstance = [super allocWithZone:zone];
});
return _singleInstance;
}
- (instancetype)init
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_singleInstance = [super init];
if (_singleInstance) {
// 在这里初始化self的属性和方法
}
});
return _singleInstance;
}
@end
这里allocWithZone:和init方法都需要返回同个对象, 所以使用static进行全局定义.
到这里已经接近尾声了, 我们可以验证一下:
SingleObject *obj1 = [[SingleObject alloc] init];
SingleObject *obj2 = [[SingleObject allocWithZone:NULL] init];
SingleObject *obj3 = [SingleObject new];
SingleObject *obj4 = [SingleObject sharedInstance];
SingleObject *obj5 = [SingleObject sharedInstance];
NSLog(@"obj1:%@", obj1);
NSLog(@"obj2:%@", obj2);
NSLog(@"obj3:%@", obj3);
NSLog(@"obj4:%@", obj4);
NSLog(@"obj5:%@", obj5);
log:
2020-06-17 14:42:12.810826+0800 KKSingletonDemo[2168:109605] obj1:<SingleObject: 0x600000740580>
2020-06-17 14:42:12.811040+0800 KKSingletonDemo[2168:109605] obj2:<SingleObject: 0x600000740580>
2020-06-17 14:42:12.811162+0800 KKSingletonDemo[2168:109605] obj3:<SingleObject: 0x600000740580>
2020-06-17 14:42:12.811325+0800 KKSingletonDemo[2168:109605] obj4:<SingleObject: 0x600000740580>
2020-06-17 14:42:12.811430+0800 KKSingletonDemo[2168:109605] obj5:<SingleObject: 0x600000740580>
最后还得注意一点, 如果单例遵循了协议, 那么创建单例的方法可能不走init而是走copy或者mutableCopy, 这种情况下我们还得添加如下处理:
#pragma mark - 如果遵循了 NSCopying / NSMutableCopying 协议
- (id)copyWithZone:(nullable NSZone *)zone
{
return _singleInstance;
}
- (id)mutableCopyWithZone:(nullable NSZone *)zone
{
return _singleInstance;
}
最后, 我们的成品代码见篇头.