深拷贝与浅拷贝是内存管理中非常重要的概念,理解好深拷贝和浅拷贝有助于加深对iOS内存管理的理解。
深拷贝与浅拷贝的概念
浅拷贝就是内存地址的复制,拷贝指向原来对象的指针,使原对象引用计数+1。可以理解为创建了一个指向原对象的新指针,并没有创建一个全新的对象。如下图:
深拷贝就是拷贝对象的具体内容,拷贝出来后两个对象虽然值是相同的,但是内存地址不一样,两个对象也互不影响:
那么怎样才能判断是深拷贝还是浅拷贝呢?有一个很简单的方法:就是通过打印对象的内存地址来判别是否是同一个对象,如果内存地址不一样则代表是新开辟的空间,为深拷贝;如果内存地址相同则代表没有开辟新的内存空间,为浅拷贝。
下面就以数组为例,来验证一下在不同情况下的深拷贝与浅拷贝:
装有基本类型元素的数组拷贝
NSArray&Copy
NSString *str2 = @"World";
NSArray *array1 = [[NSArray alloc] initWithObjects:str1, str2, nil];
id array2 = [array1 copy];
NSLog(@"array1 address: %p, array2 address: %p", array1, array2);
打印结果如下
array1 address: 0x6000037ab3e0, array2 address: 0x6000037ab3e0
结论:不可变数组进行copy,不会开辟新的内存空间也不会生成不可变对象,array1与array2指向同一个内存地址。
NSMutableArray&Copy
NSString *str1 = @"Hello";
NSString *str2 = @"World";
NSMutableArray *array1 = [[NSMutableArray alloc] initWithObjects:str1, str2, nil];
id array2 = [array1 copy];
array1[0] = @"Test";
NSLog(@"array1 address: %p, array2 address: %p", array1, array2); NSLog(@"array1: %@, array2: %@", array1, array2);
打印结果:
array1 address: 0x600002b3daa0, array2 address: 0x600002541fe0
array1: (
Test,
World
), array2: (
Hello,
World
)
array2的类型为:
结论:对可变数组进行copy,会开辟新的内存空间,生成一个新的不可变数组(这个要非常注意),两个数组互不影响。
NSArray&MutableCopy
NSString *str1 = @"Hello";
NSString *str2 = @"World";
NSArray *array1 = [[NSArray alloc] initWithObjects:str1, str2, nil];
id array2 = [array1 mutableCopy];
NSLog(@"array1 address: %p, array2 address: %p", array1, array2);
打印结果:
array1 address: 0x6000032a2fc0, array2 address: 0x600003cd2e50
array2的类型为:
结论:对不可变数组进行mutablecopy,会开辟新的内存空间,生成一个可变数组、两个数组互不影响。
NSMutableArray&mutableCopy
NSString *str1 = @"Hello";
NSString *str2 = @"World";
NSMutableArray *array1 = [[NSMutableArray alloc] initWithObjects:str1, str2, nil];
id array2 = [array1 mutableCopy];
NSLog(@"array1 address: %p, array2 address: %p", array1, array2);
打印结果:
array1 address: 0x6000038cc930, array2 address: 0x6000038cc7e0
结论:可变数组进行mutableCopy,会开辟新的内存空间,生成一个可变数组,两个数组互不影响
下面总结了一下copy和mutableCopy的结果
| copy | mutableCopy | |
|---|---|---|
| NSString | NSString浅拷贝 | NSMutableString深拷贝 |
| NSMutableString | NSString深拷贝 | NSMutableString深拷贝 |
| NSArray | NSArray浅拷贝 | NSMutableArray深拷贝 |
| NSMutableArray | NSArray深拷贝 | NSMutableArray深拷贝 |
| NSDictionary | NSDictionary浅拷贝 | NSMutableDictionary深拷贝 |
| NSMutableDictionary | NSDictionary浅拷贝 | NSMutableDictionary深拷贝 |
数组元素是可变字符串的数组拷贝
装有基本类型的数组拷贝我们已经尝试过了,那么将字符串换为可变字符串之后还会遵循上述规则么?我们来看一下:
NSMutableString *str1 = [[NSMutableString alloc] initWithString:@"hello"];
NSMutableString *str2 = [[NSMutableString alloc] initWithString:@"world"];
NSMutableArray *array1 = [[NSMutableArray alloc] initWithObjects:str1, str2, **nil**];
id array2 = [array1 mutableCopy];
[str1 appendString:@"test"];
NSLog(@"array1 address: %p, array2 address: %p", array1, array2);
NSLog(@"array1 address: %@, array2 address: %@", array1, array2);
打印结果:
array1 address: 0x600000423f90, array2 address: 0x600000423ea0
array1 address: ( hellotest, world ), array2 address: ( hellotest, world )
可以看到array2为可变数组,array1和array2的内存地址不同,证明了数组确实进行了深拷贝,但是对str1进行append操作却影响到了这两个数组,这是因为什么呢?我们来打印一下array1和array2内部数据的内存地址就知道了:
2021-11-30 15:20:46.047205+0800 Test124[73408:18976749] 0x6000022c16b0
2021-11-30 15:20:46.047354+0800 Test124[73408:18976749] 0x6000022c1950
2021-11-30 15:20:46.047495+0800 Test124[73408:18976749] --------分割线-----------
2021-11-30 15:20:46.047641+0800 Test124[73408:18976749] 0x6000022c16b0
2021-11-30 15:20:46.047763+0800 Test124[73408:18976749] 0x6000022c1950
从上面打印的数据来看可以发现两个数组存储的可变字符串的内存地址相同。数组本身进行了深拷贝,但可变字符串只是做了浅拷贝。
装有模型元素的数组拷贝
Person *p = [Person new];
p.age = 18;
p.name = @"mac";
p.sex = 1;
NSMutableArray *array1 = [NSMutableArray arrayWithObjects:p, nil];
id array2 = [array1 mutableCopy];
NSLog(@"array1 address: %p, array2 address: %p", array1, array2);
NSLog(@"array1 address: %@, array2 address: %@", array1, array2);
打印结果为:
2021-11-30 15:28:32.572374+0800 Test124[77676:18993723] array1 address: 0x6000034c6610, array2 address: 0x6000034c65b0
2021-11-30 15:28:32.572647+0800 Test124[77676:18993723] array1 address: (
"<Person: 0x600003ae8240>"
), array2 address: (
"<Person: 0x600003ae8240>"
)
结果发现与上面的可变字符串的结果相同:数组本身进行了深拷贝,但内部的对象只进行了浅拷贝
单层拷贝
对于上面的两种情况来说,数组的深拷贝并非严格意义上的深拷贝,只能算是单层深拷贝,虽然数组新开辟了内存空间,但是数组里存放的元素仍然是之前数组元素的值,并没有另外复制一份。
那么如何才能真正的实现深拷贝呢?
方法一:遍历数组,对每个元素深拷贝
Person *p1 = [Person new];
p1.age = 18;
p1.name = @"mac";
p1.sex = 1;
Person *p2 = [Person new];
p2.age = 20;
p2.name = @"jack";
p2.sex = 2;
NSMutableArray *array1 = [NSMutableArray arrayWithObjects:p1, p2, nil];
NSMutableArray *array2 = [NSMutableArray arrayWithCapacity:0];
for (Person *p **in** array1) {
[array2 addObject:p.mutableCopy];
}
NSLog(@"array1 address: %p, array2 address: %p", array1, array2);
NSLog(@"array1 address: %@, array2 address: %@", array1, array2);
打印结果:
2021-11-30 15:39:02.364721+0800 Test124[83529:19016781] array1 address: 0x6000024a5740, array2 address: 0x6000024a5860
2021-11-30 15:39:02.364979+0800 Test124[83529:19016781] array1 address: (
"<Person: 0x600002ad1b60>",
"<Person: 0x600002ad1b40>"
), array2 address: (
"<Person: 0x600002ad1b80>",
"<Person: 0x600002ad1ba0>"
)
从打印结果可以看出,array2进行了深拷贝,array2内部的元素也进行了深拷贝。同时要注意一点的是,自定义模型类需要实现NSCopying协议,否则将会崩溃。
方法二:initWithArray:copyItems
方法一这种方法需要对元素进行遍历,不是很友好。其实官方已经提供了另外的一种方式initWithArray:copyItems,这个也需要模型类实现NSCopying协议。initWithArray:copyItems时模型本身的属性都会进行深拷贝,但如果模型属性还包含数组,那这个方法就不管用了。
方法三:归档解档
归档解档的原理就是将内存中的数据写入本地文件里,当再从本地文件中读出数据时肯定是要新开辟出内存空间的。 如果模型嵌套层次较深时,可以使用归档解档进行深拷贝。使用归档解档时需要遵守NSSecureCoding协议并重写以下方法