深拷贝和浅拷贝(Shallow copy 和 Deep copy)
一.概念定义
对象复制有两种:浅拷贝和深拷贝。 普通副本是浅拷贝,它生成一个新集合,该集合与原始对象共享对象的所有权。 深层副本从原始文件创建新对象,并将其添加到新集合中。
1.浅拷贝
浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间,当内存销毁的时候,指向这片内存的几个指针需要重新定义才可以使用,要不然会成为野指针。指针拷贝,即修改A,B也会跟这改变
2.深拷贝
深拷贝是指拷贝对象的具体内容,而内存地址是自主分配的,拷贝结束之后,两个对象虽然存的值是相同的,但是内存地址不一样,两个对象也互不影响,互不干涉。分配了新内存,即A和B没有任何关系,改变A的值B也不会跟着变
3.总结:
深拷贝就是内容拷贝,浅拷贝就是指针拷贝。
本质区别在于:
- 是否开启新的内存地址(内存地址是否一致)
- 是否影响内存地址的引用计数
二.示例分析 -- 实践是检验真理的唯一标准
在iOS中深拷贝与浅拷贝要更加的复杂,涉及到容器与非容器、可变与不可变对象的copy与mutableCopy。下面用示例逐一分析:
2.1 非容器类对象的深拷贝、浅拷贝
这里指的是NSString,NSNumber等等一类的对象
2.1.1 不可变对象的copy与mutableCopy
// 非容器类 不可变对象
- (void)immutableObject {
// 1.创建一个string字符串。
NSString *string = @"Jason Mraz";
NSString *stringB = string;
NSString *stringCopy = [string copy];
NSString *stringMutableCopy = [string mutableCopy];
// 2.输出指针指向的内存地址。
NSLog(@"string = %p",string);
NSLog(@"stringB = %p",stringB);
NSLog(@"stringCopy = %p",stringCopy);
NSLog(@"stringMutableCopy = %p",stringMutableCopy);
}
//打印结果
string = 0x1085da078
stringB = 0x1085da078
stringCopy = 0x1085da078
stringMutableCopy = 0x60400005b4b0
可以看到,string、stringB和stringCopy内存地址一致,即指向的是同一块内存区域,进行了浅复制操作。而stringMutableCopy与另外三个变量内存地址不同,系统为其分配了新内存,即进行了深复制操作。
即在<非容器类 不可变对象> copy实现了浅拷贝,mutableCopy实现了深拷贝
2.1.2 可变对象的copy与mutableCopy
// 2.非容器类 可变对象
- (void)mutableObject {
// 1.创建一个可变字符串。
NSMutableString *mString = [NSMutableString stringWithString:@"Coca Cola"];
NSString *mStringCopy = [mString copy];
NSMutableString *mutablemString = [mString copy];
NSMutableString *mStringMutableCopy = [mString mutableCopy];
// 2.在可变字符串后添加字符串。
[mString appendString:@"AA"];
[mutablemString appendString:@"BB"]; // 运行时,这一行会报错。
[mStringMutableCopy appendString:@"CC"];
// 3.输出指针指向的内存地址。
NSLog(@"Memory location of \n mString = %p,\n mstringCopy = %p,\n mutablemString = %p,\n mStringMutableCopy = %p",mString, mStringCopy, mutablemString, mStringMutableCopy);
}
在上面代码中,注释2部分为可变字符串拼接字符串,运行到为mutablemString拼接字符串这一行代码时,程序会崩溃,因为通过copy方法获得的字符串是不可变字符串。所以在运行前要注释掉这一行。
//打印结果
mString = 0x608000050470,
mstringCopy = 0xa1b0ce20f6c30889,
mutablemString = 0xa1b0ce20f6c30889,
mStringMutableCopy = 0x608000050770
可以看到使用copy的对象内存地址是相同的,但所有数据都有原数据不同。所以,这里的copy和mutableCopy执行的均为深复制。
综合上面两个例子,我们可以得出这样结论:
- 对不可变对象执行copy操作,是指针复制,执行mutableCopy操作是内容复制。
- 对可变对象执行copy操作和mutableCopy操作都是内容复制。
- copy返回不可变对象,mutableCopy返回可变对象
用代码表示如下:
[immutableObject copy]; // 浅复制
[immutableObject mutableCopy]; // 深复制
[mutableObject copy]; // 深复制
[mutableObject mutableCopy]; // 深复制
2.2 容器类对象的深拷贝、浅拷贝
容器类对象指NSArray、NSDictionary等。容器类对象的深复制、浅复制如下图所示:
CopyingCollections.png
对于容器类,除了容器本身内存地址是否发生了变化外,也需要探讨的是复制后容器内元素的变化。
// 3.浅复制容器类对象
- (void)shallowCopyCollections {
// 1.创建一个不可变数组,数组内元素为可变字符串。
NSMutableString *red = [NSMutableString stringWithString:@"Red"];
NSMutableString *green = [NSMutableString stringWithString:@"Green"];
NSMutableString *blue = [NSMutableString stringWithString:@"Blue"];
NSArray *myArray1 = [NSArray arrayWithObjects:red, green, blue, nil];
// 2.进行浅复制。
NSArray *myArray2 = [myArray1 copy];
NSMutableArray *myMutableArray3 = [myArray1 mutableCopy];
NSArray *myArray4 = [[NSArray alloc] initWithArray:myArray1];
// 3.修改myArray2的第一个元素。
NSMutableString *tempString = myArray2.firstObject;
[tempString appendString:@"Color"]; //第一个元素添加Color
myMutableArray3[0] = @"no good";
[myMutableArray3 addObject:@"11111"];
[myMutableArray3 removeObjectAtIndex:1];
// 4.输出四个数组内存地址及四个数组内容。
NSLog(@"Memory location of \n myArray1 = %p, \n myArray2 %p, \n myMutableArray3 %p, \n myArray4 %p",myArray1, myArray2, myMutableArray3, myArray4);
NSLog(@"Contents of \n myArray1 %@, \n myArray2 %@, \n myMutableArray3 %@, \n myArray4 %@",myArray1, myArray2, myMutableArray3, myArray4);
}
运行demo,可以看到控制台输出如下:
myArray1 = 0x6000002402a0,
myArray2 0x6000002402a0,
myMutableArray3 0x600000240300,
myArray4 0x600000240210
Contents of
myArray1 (
RedColor,
Green,
Blue
),
myArray2 (
RedColor,
Green,
Blue
),
myMutableArray3 (
"no good",
Blue,
11111
),
myArray4 (
RedColor,
Green,
Blue
)
可以看到myArray1和myArray2数组内存地址相同,myMutableArray3和myArray4与其它数组内存地址各不相同。这是因为mutableCopy的对象会被分配新的内存,alloc会为对象分配新的内存空间。
观察数组内元素,发现修改myArray2数组内第一个元素,四个数组第一个元素都发生了改变,所以这里对于数组内元素只进行了浅复制。但通过对myMutableArray3数组内元素的增删改查发现改变时并未影响其余数组,内存地址也不与其他数组一致,即对于容器类对象进行复制操作时,深拷贝也只是单层拷贝,可以把深拷贝理解为单层深拷贝,容器内元素还是浅拷贝(指针拷贝)
2.3 自定义对象的深拷贝、浅拷贝
自定义的类需要我们自己实现NSCopying、NSMutableCopying协议,这样才可以调用copy和mutableCopy方法。如果没有遵循,拷贝时会直接Crash。
@interface Person : NSObject <NSCopying>
@property (nonatomic,copy) NSString *name;
-(id)copyWithZone:(NSZone *)zone;
@end
NSCopying协议只有一个必须实现的copyWithZone: 方法。进入Person.m,实现copyWithZone: 方法。
-(id)copyWithZone:(NSZone *)zone{
Person *copy = [[[self class] allocWithZone:zone] init];
copy->_name = [_name copy];
//copy.name = self.name;
return copy;
}
测试代码如下:
// 4.自定义对象
- (void)personCopy {
//model可变对象, 测试copy的意义:会生成新的地址,如果用=就是指向相同的地址
Person *person = [Person new];
person.name = @"qqq";
Person *newPerson = [Person new];
newPerson = [person copy];
NSLog(@"person.name=%p",person.name);
NSLog(@"newPerson.name=%p",newPerson.name);
newPerson.name = @"PPP";
NSLog(@"person.name=%p",person.name);
NSLog(@"newPerson.name=%p",newPerson.name);
}
打印结果如下:
person.name=0x10921c288
newPerson.name=0x10921c288
person.name=0x10921c288
newPerson.name=0x10921c2e8
断点结果如下:
image.png
结合打印数据和断点图片可以看到当自定义类person使用copy方法后,person.name和newPerson.name依然是相同地址,即指针拷贝,但person和newPerson地址不同,结论与可变数组一致,都为单层深拷贝
至于为什么修改了修改newPerson.name的值person.name地址一样确没有跟着改变,后面会单独一个模块讲述
三.完全深拷贝的实现
我们之前测试看到即使是深拷贝也是单层深拷贝,下面我们介绍实现完全深拷贝的方法
数组存放model(最常见的模型)
①第一种方案
// 5.数组存放自定义对象
- (void)arrayPersonCopy {
//copy两块内存地址不一样 深拷贝
Person *person = [Person new];
person.name = @"qqq";
Person *newPerson = [Person new];
newPerson.name = @"www";
//单层深拷贝 内部自定义变量还是指向同一地址 会根据内容改变
NSArray *listArr = @[person,newPerson];
NSLog(@"listArr == %@",listArr);
Person *eee = listArr[0];
eee.name = @"111";
//循环取出内部元素逐个复制
NSArray *arr = [NSArray array];
NSMutableArray *tempArr = [NSMutableArray array];
for (Person *tempP in listArr) {
Person *newPerson = [tempP copy];
[tempArr addObject:newPerson];
}
arr = tempArr;
Person *ddd = listArr[0];
ddd.name = @"asjdkasjkdakjas";
NSLog(@"%@",arr);
}
打印结果如下:
listArr == (
"<Person: 0x608000002f50>",
"<Person: 0x608000002f00>"
)
arr == (
"<Person: 0x608000002f60>",
"<Person: 0x608000002f90>"
)
可以看到数组本身和内部元素内存地址都不相同,实现了深拷贝,但还是需要注意层级问题,如果model里还有容器类对象依然存在无法完全复制的情况
②第二种方案
NSArray *arr = [[NSArray alloc] initWithArray:listArr copyItems:YES];
可以实现多层深拷贝,但必须保证容器的内部元素都可以实现了NSCopying协议,也就是实现了copyWithZone方法,不然会发生崩溃
③第三种方案
NSArray *arr = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject: listArr]];
需要在model.m中实现归档编/解码方法
// 归档需要实现的方法
// 1.编码方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"PersonName"];
}
// 2.解码方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"PersonName"];
}
return self;
}
可以使用归档功能实现深复制,可以将对象归档到一个缓冲区,然后把它从缓冲区解归档,这样就实现了深复制。
四.修改指针指向
// 6.更改指针指向地址
- (void)pointToAnotherMemoryAddress {
// 1.指针a、b同时指向字符串pro
NSString *a = @"gaoyu";
NSString *b = a;
NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
// 断点1位置
// 2.指针a指向字符串pro648
a = @"https://www.jianshu.com/u/9b0fa2e3ac62";
NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
// 断点2位置
}
断点1截图
指针a指向字符串gaoyu内存地址,b = a表示b是a的浅复制,指针b 也指向字符串gaoyu内存地址。也可以看到a和b的值是一样的
断点2截图
可以看到,a、b指针指向不同内存地址,a指向字符串https,b指向字符串gaoyu。
这是因为字符串是存在于常量区的内存数据,"="号的赋值已经更改了a元素的内存地址,但b还是指向之前的内存地址,所以a和b的内存地址已经不一样了,对应的值也不一样,自然不会因为a的改变而改变b(常量区每个字符串的内存地址都是固定的)
五.属性中copy与strong特性的区别
首先要搞清楚的就是对NSString类型的成员变量用copy修饰和用strong修饰的区别。如果使用了copy修饰符,那么在给成员变量赋值的时候就会对被赋值的对象进行copy操作,然后再赋值给成员变量。如果使用的是strong修饰符,则不会执行copy操作,直接将被赋值的变量赋值给成员变量。
假设有一个NSString类型的成员变量string,对其进行赋值:
NSString *testString = @"test";
self.string = testString;
如果该成员变量是用copy修饰的,则等价于:
self.string = [testString copy];
如果是用strong修饰的,则没有copy操作:
self.string = testString; //指针拷贝
知道了使用copy和strong的区别后,我们再来分析为什么要使用copy修饰符。先看一段代码:
NSMutableString *mutableString = [[NSMutableString alloc] initWithString:@"test"];
self.string = mutableString;
NSLog(@"%@", self.string);
[mutableString appendString:@"addstring"];
NSLog(@"%@", self.string);
如果这里成员变量string是用strong修饰的话,打印结果就是:
test
testaddstring
很显然,当mutableString的值发生了改变后,string的值也随之发生改变,因为self.string = mutableString;
这行代码实际上是执行了一次指针拷贝。string的值随mutableString的值的发生改变这显然不是我们想要的结果。
如果成员变量string是用copy修饰,打印结果就是:
test
test
这是因为使用copy修饰符后,self.string = mutableString;
就等价于self.string = [mutableString copy];
,也就是进行了一次深拷贝,所以mutableString的值再发生变化就不会影响到string的值。
结论:
NSString类型的成员变量使用copy修饰而不是strong修饰是因为有时候赋给该成员变量的值是NSMutableString类型的,这时候如果修饰符是strong,那成员变量的值就会随着被赋值对象的值的变化而变化。若是用copy修饰,则对NSMutableString类型的值进行了一次深拷贝,成员变量的值就不会随着被赋值对象的值的改变而改变。
当然,最后附送简单易懂对照表
葵花宝典
本文知识点汇总:
①深/浅拷贝的定义及理解
②copy和mutableCopy的区别与实现的拷贝效果
③完全深拷贝的实现方法
④NSString在赋值时修改了指针指向
⑤属性中copy与strong特性的区别
面试相关:
深/浅拷贝这块的知识也是一道经典面试题,如果被问到拷贝相关的知识,多半是在考验你对于内存分配,内存地址相关的内容,Good Luck
总结
No1:可变对象的copy和mutableCopy方法都是深拷贝(区别完全深拷贝与单层深拷贝) 。
No2:不可变对象的copy方法是浅拷贝,mutableCopy方法是深拷贝。
No3:copy方法返回的对象都是不可变对象。
No4:"="等号是浅拷贝,即指针拷贝
Demo下载地址:
https://github.com/gaoyuGood/copy-mutableCopy
参考文献:
深复制、浅复制、copy、mutableCopy
从源码看iOS中的深拷贝和浅拷贝
OC之数组的copy方法
iOS 图文并茂的带你了解深拷贝与浅拷贝
网友评论