集合类型的拷贝
深拷贝是深度拷贝,是拷贝一个实例对象到一个新的内存地址,而浅拷贝只是简单拷贝一个实例对象的指针。苹果官方文档提供了如下图由于理解深拷贝和浅拷贝
由上图可知,集合的浅拷贝(shallow copy)后的数组Array2与之前的数组Array1指向同一段内存区域,而深拷贝(deep copy)下Array2和Array1指向不同的内存区域。
浅拷贝
集合浅拷贝的方式有很多。当创建一个浅拷贝时,之前集合里面的每个对象都会收到retain消息,使其引用计数加1,并且将其指针拷贝到新集合中。
NSArray *array = @[@"1111"];
NSArray *shallowCopyArray = [array copyWithZone:nil];
NSLog(@"array address: %p", array);
NSLog(@"shallowCopyArray address: %p", shallowCopyArray);
NSDictionary *dic = @{@"key": @"value"};
NSDictionary *shallowCopyDic = [[NSDictionary alloc]initWithDictionary:dic copyItems:NO];
NSLog(@"dic address: %p", dic);
NSLog(@"shallowCopyDic address: %p", shallowCopyDic);
运行输出
array address: 0x60800001f700
shallowCopyArray address: 0x60800001f700
dic address: 0x6080002212c0
shallowCopyDic address: 0x6080002215c0
可以发现浅拷贝前后的数组所指向的内存地址一样,而字典所指向的内存地址发生了变化,为何同样是浅拷贝,拷贝前后地址却发生了改变呢?
这是因为对于数组,我们只是调用它的copyWithZone
方法,由于是不可变数组,返回自身,所以浅拷贝前后数组的内存地址不变。
而对于字典来说,shallowCopyDic
是通过alloc
、init
创建的,因此在内存中开辟了一段新的内存,但对于之前字典中的对象,只是拷贝其内存地址,所以浅拷贝前后字典的内存地址发生了变化,其实内部元素的地址是不变的。
因此,在集合对象的浅拷贝中,并非是对于自身的浅拷贝,而是对内部元素的拷贝
深拷贝
在深拷贝中,系统会向集合中的每一个元素发生一个copyWithZone
消息,该消息是来自NSCoping
协议,如果有对象没有实现该协议方法,那么就会奔溃,如果实现了该方法,那么会根据该方法的具体实现,实现具体的深拷贝。
NSString *str = @"222";
NSArray * array = @[str];
NSArray *shalldowCopyArray = [array copyWithZone:nil];
NSArray *deepCopyArray = [[NSArray alloc]initWithArray:array copyItems:YES];
NSLog(@"array address: %p", array);
NSLog(@"shalldowCopyArray address: %p" , shalldowCopyArray);
NSLog(@"deepCopyArray address: %p", deepCopyArray);
NSLog(@"array[0] address: %p", array[0]);
NSLog(@"shalldowCopyArray[0] address: %p", shalldowCopyArray[0]);
NSLog(@"deepCopyArray[0] address: %p", deepCopyArray[0]);
运行输出
array address: 0x6000000158a0
shalldowCopyArray address: 0x6000000158a0
deepCopyArray address: 0x6000000158b0
array[0] address: 0x10ece8158
shalldowCopyArray[0] address: 0x10ece8158
deepCopyArray[0] address: 0x10ece8158
打印前3行与我们的猜测一致,但是后面3行却打印这相同的地址。这有些意外,明明采用了深拷贝和浅拷贝,结果却是相同的内存地址,为什么会这样呢?
是因为集合类型深拷贝会对每一个元素调用copyWithZone
方法,这意味着后面3行最终打印输出什么取决于该方法。在深拷贝时,对于第一个元素调用了copyWithZone
方法,但是由于NSString是不可变的,对于其深拷贝创建一个新内存是无意义的,所以我们可以猜测NSString的copyWithZone
方法也是直接返回self
,所以浅拷贝时是直接拷贝元素地址,而深拷贝是通过copyWithZone
方法来获取元素地址,两个结果是一样的。
如果将str的类型改动一下,将其改为NSMutableString类型:
NSMutableString *str = [[NSMutableString alloc]initWithString:@"222"];
就可以看到打印的元素地址发生了改变
array[0] address: 0x6080002641c0
shalldowCopyArray[0] address: 0x6080002641c0
deepCopyArray[0] address: 0xa000000003232323
对于深拷贝来说,自身如何拷贝取决于实例方法中copyWithZone
如何实现,对于下一级还是采用浅拷贝方式,这称为集合类型的单层深拷贝
完全拷贝
除了浅拷贝和深拷贝,还有一个完全拷贝的概念。那么什么是完全拷贝呢?就是对对象的每一层都是重新创建实例变量,不存在指针拷贝。举个例子,在对数组进行归档和解档时,其实就是完全拷贝。
NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];
完全拷贝不仅是在归档、解档中存在,在其他场景也存在。
对于以上三种拷贝类型总结:
浅拷贝:在浅拷贝操作时,对于被拷贝对象的每一层进行指针拷贝
深拷贝:在深拷贝操作时,对于被拷贝对象,至少有一层是深拷贝
完全拷贝:在完成拷贝操作时,对于被拷贝对象的每一层都是深拷贝
非集合类型拷贝
不可变对象调用copy
和mutableCopy
NSString *string = @"1234";
NSString *stringCopy = [string copy];
NSMutableString *stringMCopy = [string mutableCopy];
NSLog(@"string address: %p", string);
NSLog(@"stringCopy: %p", stringCopy);
NSLog(@"stringMCopy: %p", stringMCopy);
运行输出
string address: 0x10f83a218
stringCopy: 0x10f83a218
stringMCopy: 0x600000267540
可以看到,对NSString
进行copy
只是对其指针进行的拷贝,而进行mutableCopy
是真正重新创建一个新的NSString对象。注意:写定的字符串是存放在内存的常量区,因此可以看到两处的地址位置相差甚远。
另外copy
方法是与NSCoping
协议相关的,mutableCopy
是与NSMutableCoping
协议相关的。对于NSString
这样的不可变系统类来说,copy
后返回自身是比较好理解的,所以copy
后仍然是相同的内存地址。而对NSString
调用mutableCopy
表明你需要一个新的可变对象,所以会返回一个NSMutableString
对象。
可变对象的copy
和mutableCopy
NSMutableString *mString = [[NSMutableString alloc]initWithString:@"123"];
NSString *copyString = [mString copy];
NSString * mCopyString = [mString mutableCopy];
NSLog(@"mString address: %p", mString);
NSLog(@"copyString address: %p", copyString);
NSLog(@"mCopyString address: %p", mCopyString);
NSLog(@"copyString is mutable: %@", [copyString isKindOfClass:[NSMutableString class]] ? @"YES": @"No");
NSLog(@"mCopyString is mutable: %@", [mCopyString isKindOfClass:[NSMutableString class]] ? @"YES": @"No");
运行输出
mString address: 0x600000073e00
copyString address: 0xa000000003332313
mCopyString address: 0x600000074100
copyString is mutable: No
mCopyString is mutable: YES
从打印结果来看,对于NSMutableString
来说,其copy
的返回值是一个不可变的字符串,而mutableCopy
返回的是可变字符串,即使三者是不一样的地址,即三个对象。
在Foundation
和UIkit
框架中,类似于NSString
和NSMutableString
这样的非集合对象分为可变和不可变对象并不多,但是对于copy
和mutableCopy
的实现原理都是一样的,即:对可变对象的copy
是不可变对象,使用mutableCopy
都为可变
[immutableObject copy]; //浅复制
[immutableObject mutableCopy]; //深复制
[mutableObject copy]; //深复制
[mutableObject mutableCopy]; //深复制
自定义拷贝
如果自定义的类需要实现浅拷贝,则在实现copyWithZone
方法是返回自身即可,而如果需要实现深拷贝,则在copyWithZone
方法中创建一个新实例对象返回即可
对于所谓的深拷贝,其实应当取决于每一层对象本身,如果需要达到完全深拷贝,则每一层对象都应当在copyWithZone
方法中创建好新的实例对象,如果每一层都为深拷贝,那么最外层拷贝就是完全拷贝了。
copy属性
在类中声明属性时有个copy
修饰符,一般用于修饰字符串和block
以及一些数组和字典。那么为什么要声明为copy
,而不是声明为strong
呢?
@property(nonatomic, copy) NSMutableString *string;
self.string = [[NSMutableString alloc]initWithString:@"1234"];
NSString *copyString = self.string;
NSLog(@"copyString is mutable: %@", [copyString isKindOfClass:[NSMutableString class]] ? @"YES": @"NO");
NSLog(@"string is mutable: %@", [self.string isKindOfClass:[NSMutableString class]] ? @"YES": @"NO");
运行输出
copyString is mutable: NO
string is mutable: NO
但是当我们把copy
改为strong
,则会打印YES
.
这其实并不复杂,在使用copy
的时候,会对属性的setter
方法进行判断,对属性进行copy
,根据属性是否可变,则与前面说到的逻辑相同,如果属性可变,则返回一个新的不可变对象,即为不可变字符串,而对于不可变则直接返回self,即为不可变字符串
如果是strong
,则是直接对其引用,并没有执行copy
方法,这就是区别。如果属性缓存数组或者字典,原理是一样的。
对于block
稍微有些不同,因为MRC
中,block
需要显示地copy
到堆上,而ARC
中如果引用外部变量赋值则会自动拷贝到内存中,所以block
在ARC
下使用copy
和strong
无异。
对于NSString
来说,作为不可变对象,修饰符为copy
时,执行copy
方法返回自身,strong
修饰也是返回自身,所以对于NSString
这样的不可变对象来说,使用strong
和copy
也是一样的。
参考