iOS单例的正确使用姿势

单例,是我们在开发中使用非常多的一种设计模式。但是,我们真的有写好一个单例么?这篇文章记录了单例的一些常见问题。

  • 常见的单例写法及存在的问题
  • 好的单例怎么写
  • 单例的特殊需求

普通写法

  • 单例代码
1
2
3
4
5
6
7
8
9
10
11
@implementation SingletonClass

+ (instancetype)shareInstance {
static SingletonClass *_sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}
@end
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MARK : singleton
- (void)testSingleton {
SingletonClass *cls1 = [SingletonClass shareInstance];
NSLog(@"cls1 = %@", cls1);

SingletonClass *cls2 = [SingletonClass shareInstance];
NSLog(@"cls2 = %@", cls2);

SingletonClass *cls3 = [[SingletonClass alloc] init];
NSLog(@"cls3 = %@", cls3);

SingletonClass *cls4 = [cls1 copy];
NSLog(@"cls4 = %@", cls4);
}
  • 测试结果
1
2
3
4
5
Test[4588:4307534] cls1 = <SingletonClass: 0x600002688150>
Test[4588:4307534] cls2 = <SingletonClass: 0x600002688150>
Test[4588:4307534] cls3 = <SingletonClass: 0x6000026a0260>
Test[4588:4307534] -[SingletonClass copyWithZone:]: unrecognized selector sent to instance 0x600002688150

  • 分析

  • 当我们调用 shareInstance 方法时获取到的对象是相同的;

  • 当我们通过 alloc 和 init 来构造对象的时候,得到的对象却是不一样的;

  • 如果直接使用 copy, 会在此崩溃。

所以,当我们通过不同的途径获取对象,并不能保证对象的唯一性,,所以我们就需要封锁用户通过 alloc 和 init 以及 copy 来构造对象这条道路。

完善写法

  • 单例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation SingletonClass
+ (instancetype)shareInstance {
static SingletonClass *_sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[super allocWithZone:NULL] init];
});
return _sharedInstance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self shareInstance];
}

- (id)copyWithZone:(nullable NSZone *)zone {
return [SingletonClass shareInstance];
}
@end
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)testSingleton {
SingletonClass *cls1 = [SingletonClass shareInstance];
NSLog(@"cls1 = %@", cls1);

SingletonClass *cls2 = [SingletonClass shareInstance];
NSLog(@"cls2 = %@", cls2);

SingletonClass *cls3 = [[SingletonClass alloc] init];
NSLog(@"cls3 = %@", cls3);

SingletonClass *cls4 = [cls1 copy];
NSLog(@"cls4 = %@", cls4);
}
  • 测试结果
1
2
3
4
Test[4793:4317590] cls1 = <SingletonClass: 0x600003e8c460>
Test[4793:4317590] cls2 = <SingletonClass: 0x600003e8c460>
Test[4793:4317590] cls3 = <SingletonClass: 0x600003e8c460>
Test[4793:4317590] cls4 = <SingletonClass: 0x600003e8c460>
  • 分析

上方的几个问题这种写法都解决了。

特殊的需求

可继承的单例

  • 说明

因为单例具有唯一性,要保证每个创建的类是不同的,所以在每个类生成时,我们动态的给类绑定唯一的对象。

  • 单例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import <objc/runtime.h>

@implementation InheritableSharedInstanceClass

+ (instancetype)sharedInstance {
id sharedInstance = objc_getAssociatedObject(self, @"SharedInstance");
if (!sharedInstance) {
sharedInstance = [[super allocWithZone:NULL] init];
objc_setAssociatedObject(self, @"SharedInstance", sharedInstance, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return sharedInstance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}

- (id)copyWithZone:(nullable NSZone *)zone {
return [[self class] sharedInstance];
}

@end
  • 测试代码
1
2
3
4
5
6
7
- (void)testSingleton {
SingletonClass *cls = [SingletonClass shareInstance];
NSLog(@"cls = %@", cls);

SubSingletonClass *subCls = [SubSingletonClass shareInstance];
NSLog(@"subCls = %@", subCls);
}
  • 测试结果
1
2
cls = <SingletonClass: 0x6000017ebb10>
subCls = <SubSingletonClass: 0x6000017cc4d0>

单例的销毁

  • 说明

虽然单例销毁需要销毁的情况还是比较少的,这里还是加一下,防止特殊的需求出现。

  • 单例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static dispatch_once_t onceToken;
static SingletonClass *_sharedInstance = nil;

@implementation SingletonClass
+ (instancetype)shareInstance {
dispatch_once(&onceToken, ^{
_sharedInstance = [[super allocWithZone:NULL] init];
});
return _sharedInstance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self shareInstance];
}

- (id)copyWithZone:(nullable NSZone *)zone {
return [SingletonClass shareInstance];
}

+ (void)shouldDealloc {
onceToken = 0;
_sharedInstance = nil;
}
@end
  • 测试代码
1
2
3
4
5
6
7
8
9
- (void)testSingleton {
SingletonClass *clsOld = [SingletonClass shareInstance];
NSLog(@"clsOld = %@", clsOld);

[SingletonClass shouldDealloc];

SingletonClass *clsNew = [SingletonClass shareInstance];
NSLog(@"clsNew = %@", clsNew);
}
  • 测试结果
1
2
clsOld = <SingletonClass: 0x600003e3d9b0>
clsNew = <SingletonClass: 0x600003e3d9a0>
  • 分析

必须把 static dispatch_once_t onceToken; 这个拿到函数体外,成为全局的。

onceToken 置成 0,GCD 才会认为它从未执行过.它默认为 0,这样才能保证下次再次调用 shareInstance 的时候,再次创建对象。

  • Swift 中单例的写法
1
2
3
4
class SingletonClass {
static let sharedInstance = SingletonClass()
private init() {} // 初始化方法设置为私有,防止被直接使用初始化方法创建
}