如何用OC如何写一个单例

前言

  最近看了一些copyWithZone:这个方法相关的一些东西.没想到从这里可以延伸出来很多内容,包括深拷贝,浅拷贝,copy,mutableCopy,NSCopying协议,NSMutableCoping协议,单例等等东西.他们之间要么相互关联,要么环环相扣.也许拿出其中一点来可以说的比较清晰,但是全部合在一起又很乱了.因此写一篇文章来梳理一下这些知识脉络.

单例模式

  首先从单例开始说起,因为单例里面涉及到上面比较多的东西.
  单例指的是在app生命周期内只会存在一个实例的对象.无论实例化多少次,都只会在内存中存在一个地址.现在看一个简单的单例模式.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
///.h
@interface SJPSingletonManager:NSObject
+ (instancetype)shareManager;
@end
///.m
@implementation SJPSingletonManager

+(instancetype)shareManager {
static SJPSingletonManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[SJPSingletonManager alloc] init];
});
return manager;
}

@end
.....
///testCode
SJPSingletonManager *manager1 = [SJPSingletonManager shareManager];
SJPSingletonManager *manager2 = [SJPSingletonManager shareManager];
/// consle outprint
2022-01-07 15:29:50.053345+0800 multi-thread-demo[87156:1046260] singleton:0x600002830000--0x600002830000

  通过dispatch_once_t这种方式让初始化代码只执行一次.因此做到了多次实例化只得到一份内存的效果,但是这么写的缺点在于如果调用的人不使用暴露出来的shareManager方法,而是通过init或者new的方式进行初始化的话,得到的对象就不在是一个单例,因此需要把这两个入口也堵住.另外还了解到,无论是通过init的方式,还是通过new的方式创建对象,最终都会调用到allocWithZone:这个方法(经过验证确实是这样子的),因此我们可以少写一些代码,直接堵住allocWithZone:这个初始化方法.因此修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (instancetype)shareManager {
return [[self alloc] init];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static SJPSingletonManager *_manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!_manager) {
_manager = [super allocWithZone:zone];
}
});
return _manager;
}

  shareManager内部不在执行dispatch方法,而是放到allocWithZone:中执行.个人认为到这一步的话已经可以算是完整的单例了,但是还是有一些场景需要考虑下.

  • 如果调用方使用copy来对单例进行复制.
  • 如果调用方方使用mutableCopy来对单例进行复制.
      copy和mutableCopy都是定义在NSCopying和NSMutableCopying协议中的方法,如果不继承这两个协议直接调用copy方法的话,会直接调用copyWithZone:这个方法从而造成crash.crash信息如下:
  • Thread 1: “-[SJPSingletonManager copyWithZone:]: unrecognized selector sent to instance 0x600001328980”

  此处我发现了一个华点,如果不继承NSCopying协议,直接实现copyWithZone:这个方法,在实例对象调用copy方法时也不会crash,因此推测NSObject内部的实现代码大致是这样的:

1
2
///没有前面的if条件判断
[self performSelector:@selector(copyWithZone:)];

  增加了copyWithZone:mutableCopyWithZone:方法后,整体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@implementation SJPSingletonManager

+(instancetype)shareManager {
return [[self alloc] init];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static SJPSingletonManager *_manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!_manager) {
_manager = [super allocWithZone:zone];
}
});
return _manager;
}

- (nonnull id)copyWithZone:(nullable NSZone *)zone {
return [SJPSingletonManager allocWithZone:zone];
}


- (nonnull id)mutableCopyWithZone:(nullable NSZone *)zone {
return [SJPSingletonManager allocWithZone:zone];
}

@end

  到这一步应该已经算是一个完整的单例了.涵盖了可能想到的所有场景.

copy,mutableCopy

  oc提供的集合对象:array,dictionary,string等内部都是实现了<NSCopying,NSMutableCopying>两个协议的,有一点需要明确,以上这些类型通过调用mutableCopy方法得到的一定是可变对象,通过copy方法调用得到的一定是immutable对象.考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
///.h
@interface classA : NSObject<NSCopying,NSMutableCopying>
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, copy) NSString *str;
@property (nonatomic, copy) NSArray *iArray;
@end


///.m
- (instancetype)init
{
self = [super init];
if (self) {
_iArray = @[@"111",@"222"];
_str = @"ssss";
_array = @[@"3333"].mutableCopy;
}
return self;
}

- (nonnull id)copyWithZone:(nullable NSZone *)zone {
classA *A = [[classA alloc] init];
A.iArray = self.iArray.copy;
A.str = self.str.copy;
A.array = self.array.copy;
NSLog(@"copy--%p,%p,---array:%p,%p",self.str,A.str,self.array,A.array);
return A;
}

- (nonnull id)mutableCopyWithZone:(nullable NSZone *)zone {
classA *A = [[classA alloc] init];
A.iArray = self.iArray.mutableCopy;
A.str = self.str.mutableCopy;
A.array = self.array.copy;
return A;
}
@end

....
classA *A = [classA new];
classA *Aa = [A copy];
classA *Aaa = [A mutableCopy];

  通过上面的方式定义了copyWithZone和mutableCopyWithZone方法之后,我们可以对自定义的对象进行拷贝.得到的地址不同的对象.增加这个方法之后就可以对自定义的对象使用copy和mutableCopy方法.

深拷贝,浅拷贝

  前面说过,执行copy后得到的是不可变对象,执行mutableCopy方法之后得到的时候可变对象.这在深拷贝和浅拷贝上表现出来是这样的.上面代码运行后,可以得到log输出
2022-01-07 17:50:05.046960+0800 multi-thread-demo[90687:1165918] copy–0x10260c5b8,0x10260c5b8,—array:0x6000008670f0,0x6000004fab00,

  分析:

  • str是一个不可变的string对象,使用copy是浅拷贝,因此得到的是一份地址的拷贝且内容相同.以前曾经疑问一个点,虽然地址相同,但是对其中一个重新赋值之后为什么地址又不同呢,指向的内容也不同了呢,现在才发现,不可变对象本身是不可修改的,重新赋值只会重新申请一块内存存储新的对象.
  • array是一个可变对象,使用copy之后获得一个不可变对象,不过也会生成新的数据地址.同时,使用mutableCopy是深拷贝,这里有一个点,一般开发过程中建议把对象声明为不可变的,如果在运行过程中一定需要修改对象内的元素的话,可以通过如下方式:
    1
    2
    3
    4
    5
    _iArray = @[@"111",@"222"];
    NSMutableArray *mArray = [_iArray mutableCopy];
    [mArray setValue:@"222" forKey:@"1111"];
    _iArray = mArray.copy;
    /// 通过借助第三个变量来对不可变的变量进行修改.