iOS 可变拷贝VS不可变拷贝
概念
我们先来了解两个概念
深拷贝deep copy
: 直接拷贝整个对象内存到另一块内存中;
浅拷贝shallow copy
: 并不拷贝对象本身,仅仅是拷贝指向对象的指针;
详细介绍看下面这段Apple Documents描述;
The normal copy is a shallow copy that produces a new collection that shares ownership of the objects with the original. Deep copies create new objects from the originals and add those to the new collection.
字符串
我们分成2种情况测试:
- 初始值不可变
NSString *originStr = @"hello";
NSString *copyStr = [originStr copy];
NSString *mulCopyStr = [originStr mutableCopy];
NSLog(@"originStr address: %p", originStr);
NSLog(@"copyStr address: %p", copyStr);
NSLog(@"mulCopyStr address: %p", mulCopyStr);
2017-09-21 13:36:08.859 iOSLockDemo[35388:2491304] originStr address: 0x1021e6078
2017-09-21 13:36:08.859 iOSLockDemo[35388:2491304] copyStr address: 0x1021e6078
2017-09-21 13:36:08.860 iOSLockDemo[35388:2491304] mulCopyStr address: 0x608000078000
通过log可以发现,此时copy
是浅拷贝,mutableCopy
是深拷贝;
- 初始值可变
NSString *originStr = [@"hello" mutableCopy];
NSString *copyStr = [originStr copy];
NSString *mulCopyStr = [originStr mutableCopy];
NSLog(@"originStr address: %p", originStr);
NSLog(@"copyStr address: %p", copyStr);
NSLog(@"mulCopyStr address: %p", mulCopyStr);
2017-09-21 13:39:33.036 iOSLockDemo[35469:2498312] originStr address: 0x60800007bd40
2017-09-21 13:39:33.037 iOSLockDemo[35469:2498312] copyStr address: 0xa00006f6c6c65685
2017-09-21 13:39:33.037 iOSLockDemo[35469:2498312] mulCopyStr address: 0x60800007c000
通过log可以发现,此时copy
、mutableCopy
都是深拷贝;
集合
- 初始值为不可变集合
NSArray *originArr = @[@"item1"];
NSLog(@"originArr address: %p", originArr);
NSLog(@"originArr item address: %p", originArr[0]);
NSArray *copyArr = [originArr copy];
NSLog(@"copyArr address: %p", copyArr);
NSLog(@"copyArr item address: %p", copyArr[0]);
NSArray *mulCopyArr = [originArr mutableCopy];
NSLog(@"mulCopyArr address: %p", mulCopyArr);
NSLog(@"mulCopyArr item address: %p", mulCopyArr[0]);
2017-09-21 13:48:16.515 iOSLockDemo[35683:2513469] originArr address: 0x600000006ca0
2017-09-21 13:48:16.516 iOSLockDemo[35683:2513469] originArr item address: 0x10d338080
2017-09-21 13:48:16.516 iOSLockDemo[35683:2513469] copyArr address: 0x600000006ca0
2017-09-21 13:48:16.517 iOSLockDemo[35683:2513469] copyArr item address: 0x10d338080
2017-09-21 13:48:16.517 iOSLockDemo[35683:2513469] mulCopyArr address: 0x608000244800
2017-09-21 13:48:16.518 iOSLockDemo[35683:2513469] mulCopyArr item address: 0x10d338080
通过Log,我们可以发现,当初始值不可变时
copy
操作,无论集合本身,还是集合里的objects
都是指针拷贝,这时我们可以称这个过程是浅拷贝。
mutableCopy
操作,集合指针发生了变化,说明生成了新的集合,但是集合里的objects
指针还是和原来一样。那么这时我们还能称之为深拷贝吗?
下面我们引入一个概念单层深拷贝one-level-deep copy
;什么意思呢?下面我们看一段代码就明白了;
Apple Documents提供了一个进行one-level-deep copy
的方法。
[[NSArray alloc] initWithArray:<#(nonnull NSArray *)#> copyItems:<#(BOOL)#>]
You can use the collection’s equivalent of initWithArray:copyItems: with YES as the second parameter. If you create a deep copy of a collection in this way, each object in the collection is sent a copyWithZone: message. If the objects in the collection have adopted the NSCopying protocol, the objects are deeply copied to the new collection, which is then the sole owner of the copied objects. If the objects do not adopt the NSCopying protocol, attempting to copy them in such a way results in a runtime error.
用这种方法深复制,集合里的每个对象都会收到copyWithZone:
消息。如果集合里的对象遵循NSCopying
协议,那么对象就会被深拷贝(deep copy)
到新的集合,并且这个新的集合是被拷贝对象的唯一所有者,如果对象没有遵循 NSCopying 协议,而尝试用这种方法进行深复制,会在运行时出错。
看到这里,是不是觉得生活很美好,这方法使用很简单?too young too naive;下面我们看几个例子;
- 集合
不可变
,集合内元素为非集合
对象,并且集合内元素为不可变
对象。
NSArray *originArr = @[@"11", @"22"];
NSLog(@"originArr address: %p", originArr);
[self print:originArr arrName:@"originArr"];
NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"copyArr address: %p", copyArr);
[self print:copyArr arrName:@"copyArr"];
NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"mutCopyArr address: %p", mutCopyArr);
[self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] originArr address: 0x608000029180
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] originArr subArr item 0 address: 0x10f012080
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] originArr subArr item 1 address: 0x10f0120a0
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] copyArr address: 0x600000028600
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] copyArr subArr item 0 address: 0x10f012080
2017-09-21 15:58:53.074 iOSLockDemo[38249:2650494] copyArr subArr item 1 address: 0x10f0120a0
2017-09-21 15:58:53.074 iOSLockDemo[38249:2650494] mutCopyArr address: 0x60000005c5f0
2017-09-21 15:58:53.074 iOSLockDemo[38249:2650494] mutCopyArr subArr item 0 address: 0x10f012080
2017-09-21 15:58:53.074 iOSLockDemo[38249:2650494] mutCopyArr subArr item 1 address: 0x10f0120a0
通过log发现了什么?无论可变拷贝,还是非可变拷贝,进行深拷贝的仍然只是集合本身,而集合内的元素还是指针拷贝,这下懵了,说好的集合内对象进行深拷贝呢?
再看下面一个例子
- 集合
不可变
,集合内元素为非集合
对象,并且集合内元素为可变
对象。
NSArray *originArr = @[[@"11" mutableCopy], [@"22" mutableCopy]];
NSLog(@"originArr address: %p", originArr);
[self print:originArr arrName:@"originArr"];
NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"copyArr address: %p", copyArr);
[self print:copyArr arrName:@"copyArr"];
NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"mutCopyArr address: %p", mutCopyArr);
[self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 16:06:16.961 iOSLockDemo[38399:2658835] originArr address: 0x60000003a900
2017-09-21 16:06:16.964 iOSLockDemo[38399:2658835] originArr subArr item 0 address: 0x60000007c9c0
2017-09-21 16:06:16.964 iOSLockDemo[38399:2658835] originArr subArr item 1 address: 0x60000007cc80
2017-09-21 16:06:16.964 iOSLockDemo[38399:2658835] copyArr address: 0x60000003a920
2017-09-21 16:06:16.965 iOSLockDemo[38399:2658835] copyArr subArr item 0 address: 0xa000000000031312
2017-09-21 16:06:16.965 iOSLockDemo[38399:2658835] copyArr subArr item 1 address: 0xa000000000032322
2017-09-21 16:06:16.965 iOSLockDemo[38399:2658835] mutCopyArr address: 0x60800005e390
2017-09-21 16:06:16.966 iOSLockDemo[38399:2658835] mutCopyArr subArr item 0 address: 0xa000000000031312
2017-09-21 16:06:16.966 iOSLockDemo[38399:2658835] mutCopyArr subArr item 1 address: 0xa000000000032322
通过log发现,无论可变拷贝,还是非可变拷贝,集合本身和集合内对象都进行了深拷贝,但是两次拷贝的结果中,集合内对象的指针地址居然相同(ps: 多次测试,仍然如此,有兴趣的同学,可以多加几组拷贝试试),这下更懵了。
- 集合
可变
,集合内元素为非集合
对象,并且集合内元素为不可变
对象
NSArray *originArr = [@[@"11", @"2"] mutableCopy];
NSLog(@"originArr address: %p", originArr);
[self print:originArr arrName:@"originArr"];
NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"copyArr address: %p", copyArr);
[self print:copyArr arrName:@"copyArr"];
NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"mutCopyArr address: %p", mutCopyArr);
[self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 16:15:27.249 iOSLockDemo[38637:2669678] originArr address: 0x60000005a880
2017-09-21 16:15:27.249 iOSLockDemo[38637:2669678] originArr subArr item 0 address: 0x10baf8080
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] originArr subArr item 1 address: 0x10baf80a0
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] copyArr address: 0x608000035580
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] copyArr subArr item 0 address: 0x10baf8080
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] copyArr subArr item 1 address: 0x10baf80a0
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] mutCopyArr address: 0x60800005e5a0
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] mutCopyArr subArr item 0 address: 0x10baf8080
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] mutCopyArr subArr item 1 address: 0x10baf80a0
通过log,这组结果和第一组是一样的。好,我们再看一组实验。
- 集合
可变
,集合内元素为非集合
对象,并且集合内元素为可变
对象
NSArray *originArr = [@[[@"11" mutableCopy], [@"22" mutableCopy]] mutableCopy];
NSLog(@"originArr address: %p", originArr);
[self print:originArr arrName:@"originArr"];
NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"copyArr address: %p", copyArr);
[self print:copyArr arrName:@"copyArr"];
NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"mutCopyArr address: %p", mutCopyArr);
[self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 16:17:48.706 iOSLockDemo[38703:2673443] originArr address: 0x6000000482b0
2017-09-21 16:17:48.706 iOSLockDemo[38703:2673443] originArr subArr item 0 address: 0x600000267600
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] originArr subArr item 1 address: 0x6000002678c0
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] copyArr address: 0x60800003d120
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] copyArr subArr item 0 address: 0xa000000000031312
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] copyArr subArr item 1 address: 0xa000000000032322
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] mutCopyArr address: 0x608000046510
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] mutCopyArr subArr item 0 address: 0xa000000000031312
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] mutCopyArr subArr item 1 address: 0xa000000000032322
这组实验结果和第二组一样。是不是彻底懵了?难道是官方文档写错了?好,那我们自定义一个对象,并实现NSCopying
协议,然后在集合中使用我们自定义的对象。
#import <Foundation/Foundation.h>
@interface Person : NSObject<NSCopying>
@end
#import "Person.h"
@implementation Person
- (id)copyWithZone:(nullable NSZone *)zone {
return [Person allocWithZone:zone];
}
@end
NSArray *originArr = @[[Person new], [Person new]];
NSLog(@"originArr address: %p", originArr);
[self print:originArr arrName:@"originArr"];
NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"copyArr address: %p", copyArr);
[self print:copyArr arrName:@"copyArr"];
NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"mutCopyArr address: %p", mutCopyArr);
[self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 16:33:46.501 iOSLockDemo[39061:2691963] originArr address: 0x60000003cc60
2017-09-21 16:33:46.501 iOSLockDemo[39061:2691963] originArr subArr item 0 address: 0x60000001d790
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] originArr subArr item 1 address: 0x60000001d7a0
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] copyArr address: 0x60800003d2e0
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] copyArr subArr item 0 address: 0x60800001d2d0
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] copyArr subArr item 1 address: 0x60800001d2a0
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] mutCopyArr address: 0x60800005fda0
2017-09-21 16:33:46.503 iOSLockDemo[39061:2691963] mutCopyArr subArr item 0 address: 0x60800001d2b0
2017-09-21 16:33:46.503 iOSLockDemo[39061:2691963] mutCopyArr subArr item 1 address: 0x60800001d2c0
好神奇,这次结果和苹果官方文档写的一致了。这是为什么呢?
[[NSArray alloc] initWithArray: copyItems:
得到的最终结果其实是和copyWithZone:
的实现方式息息相关的,之所以集合中是字符串时会出现和官方文档相左的结果;
以下是个人猜测,望了解的同学可以指点一二。
因为NSString在实现copyWithZone:
根据不同的情况作了优化。如果当前是可变对象,则返回一个不可变的副本,如果当前对象是不可变对象,则返回当前对象。**
**至于第二次实验中两次拷贝结果中的容器中的对象地址相同的情况,是NSArray
在实现[[NSArray alloc] initWithArray: copyItems:
为了节约内存开销,进行了优化。当第一次调用时,会给集合内对象发送copyWithZone:
生成新的不可变副本,以后调用时,不再向原对象发送消息,而是使用第一次生成的不可变副本。
下面我们测试一下多层集合嵌套的情况。
NSArray *originArr = @[@[[Person new], [Person new]]];
NSLog(@"originArr address: %p", originArr);
[self print:originArr arrName:@"originArr"];
NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"copyArr address: %p", copyArr);
[self print:copyArr arrName:@"copyArr"];
NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
NSLog(@"mutCopyArr address: %p", mutCopyArr);
[self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 17:16:34.672 iOSLockDemo[39913:2737590] originArr address: 0x600000005670
2017-09-21 17:16:34.672 iOSLockDemo[39913:2737590] originArr subArr address: 0x60000002d820
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] originArr subArr item 0 address: 0x600000005650
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] originArr subArr item 1 address: 0x600000005660
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] copyArr address: 0x600000005610
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] copyArr subArr address: 0x60000002d820
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] copyArr subArr item 0 address: 0x600000005650
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] copyArr subArr item 1 address: 0x600000005660
2017-09-21 17:16:34.674 iOSLockDemo[39913:2737590] mutCopyArr address: 0x600000057970
2017-09-21 17:16:34.674 iOSLockDemo[39913:2737590] mutCopyArr subArr address: 0x60000002d820
2017-09-21 17:16:34.674 iOSLockDemo[39913:2737590] mutCopyArr subArr item 0 address: 0x600000005650
2017-09-21 17:16:34.674 iOSLockDemo[39913:2737590] mutCopyArr subArr item 1 address: 0x600000005660
通过log,可以发现,只有最外层的集合地址发生了变化,其他的不变。
由此我们得出结论:
one-level-deep copy
所谓的单层深拷贝
的意思是,只有非嵌套集合,且集合内对象在实现copyWithZone:
时返回新的对象时,才会产生真正意义上的深拷贝,也就是完全拷贝(real-deep copy)
。
那么怎样才能实现真正意义上的深拷贝呢?看下面实现。
完全拷贝
NSArray *originArr = @[@[[@"11" mutableCopy], [@"22" mutableCopy]]];
NSLog(@"originArr address: %p", originArr);
[self print:originArr arrName:@"originArr"];
NSArray* copyArr = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject:originArr]];
NSLog(@"copyArr address: %p", copyArr);
[self print:copyArr arrName:@"copyArr"];
NSArray* mutCopyArr = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject:originArr]];
NSLog(@"mutCopyArr address: %p", mutCopyArr);
[self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 17:27:49.138 iOSLockDemo[40148:2754251] originArr address: 0x6080000025d0
2017-09-21 17:27:49.138 iOSLockDemo[40148:2754251] originArr subArr address: 0x608000026ce0
2017-09-21 17:27:49.139 iOSLockDemo[40148:2754251] originArr subArr item 0 address: 0x608000072980
2017-09-21 17:27:49.139 iOSLockDemo[40148:2754251] originArr subArr item 1 address: 0x6080000729c0
2017-09-21 17:27:49.139 iOSLockDemo[40148:2754251] copyArr address: 0x600000002550
2017-09-21 17:27:49.140 iOSLockDemo[40148:2754251] copyArr subArr address: 0x600000027780
2017-09-21 17:27:49.140 iOSLockDemo[40148:2754251] copyArr subArr item 0 address: 0x600000077600
2017-09-21 17:27:49.140 iOSLockDemo[40148:2754251] copyArr subArr item 1 address: 0x600000077740
2017-09-21 17:27:49.140 iOSLockDemo[40148:2754251] mutCopyArr address: 0x600000002570
2017-09-21 17:27:49.141 iOSLockDemo[40148:2754251] mutCopyArr subArr address: 0x600000027620
2017-09-21 17:27:49.141 iOSLockDemo[40148:2754251] mutCopyArr subArr item 0 address: 0x600000077540
2017-09-21 17:27:49.141 iOSLockDemo[40148:2754251] mutCopyArr subArr item 1 address: 0x600000077680
通过log,可以发现,这次是完全拷贝
了
结语
对其他集合类型NSSet,NSDictionary也具有以上特点。有兴趣的同学可以试试。