当你的才华撑不起你的野心时,你就应该静下来学习。 —— CJJ
Why Learn?
浅拷贝和深拷贝是必须要掌握的知识点,工作中也会频繁用到,所以在这里记录总结一下自己的理解,方便日后查看。
Ask?
如果你觉得浅拷贝就是copy
,深拷贝就是mutableCopy
的话,那就大错特错了,请继续往下看
定义
首先浅拷贝是什么?深拷贝又是什么呢?
这两种都是拷贝(复制)对象的意思,不同的是
- 浅拷贝在拷贝对象的过程中
- 只拷贝了指向对象内存地址的指针
- 拷贝后新的指针仍然指向原来的内存地址。
- 深拷贝在拷贝对象的过程中
- 不仅拷贝了指向对象内存地址的指针
- 还会拷贝原指针所指向的内存地址所存储的内容
- 并且在堆中重新生成一块内存地址来存放所拷贝的内容
- 然后新的指针会指向新的内存地址。
简单对比
浅拷贝:拷贝指针,不开辟内存地址
深拷贝:拷贝指针和内容,开辟内存地址(存放拷贝的内容)
如何判断?
要判断一个拷贝是浅拷贝还是深拷贝,不仅看copy
还是mutableCopy
,还要考虑拷贝的对象是可变的还是不可变的
举例说明,假如存在不可变对象A,可变对象B,对A和B分别进行
copy
和mutableCopy
,结果如下
-
例子一:对不可变对象A 进行
copy
NSString *A = @"aaa";
id C = [A copy];
NSLog(@"\n不可变-copy\n%p = 原对象指针地址\n%p = copy的对象指针地址\n%p = 原对象的内存地址 - %@\n%p = copy的对象的内存地址 - %@",&A,&C,A,A,C,C);
控制台打印
2020-06-03 17:52:29.341114+0800 Demo[12665:880626]
不可变-copy
0x7ffee06c70e0 = 原对象指针地址
0x7ffee06c70d8 = copy的对象指针地址
0x10f53d358 = 原对象的内存地址 - aaa
0x10f53d358 = copy的对象的内存地址 - aaa
从打印结果我们可以看出,对不可变对象进行
copy
后
- 指向内存地址的指针地址不一样
- 内存地址一样(存放的内容都是
aaa
)所以这是一个
浅拷贝
。
-
例子二:对不可变对象A进行mutableCopy
NSString *A = @"aaa";
id C = [A mutableCopy];
NSLog(@"\n不可变-mutableCopy\n%p = 原对象指针地址\n%p = copy的对象指针地址\n%p = 原对象的内存地址 - %@\n%p = copy的对象的内存地址 - %@",&A,&C,A,A,C,C);
控制台打印
2020-06-03 17:53:01.600945+0800 Demo[12679:881248]
不可变-mutableCopy
0x7ffee97830e0 = 原对象指针地址
0x7ffee97830d8 = copy的对象指针地址
0x106481358 = 原对象的内存地址 - aaa
0x60000209ab20 = copy的对象的内存地址 - aaa
从打印结果我们可以看出,对不可变对象进行
mutableCopy
后
- 指向内存地址的指针地址不一样
- 内存地址不一样,开辟了新的内存地址(存放的内容都是
aaa
)所以这是一个
深拷贝
-
例子三:对可变对象B进行copy
NSMutableString *B = [NSMutableString stringWithString:@"bbb"];
id C = [B copy];
NSLog(@"\n可变-copy\n%p = 原对象指针地址\n%p = copy的对象指针地址\n%p = 原对象的内存地址 - %@\n%p = copy的对象的内存地址 - %@",&B,&C,B,C,B,C);
控制台打印
2020-06-03 17:51:22.168009+0800 Demo[12647:879529]
可变-copy
0x7ffedfc920e0 = 原对象指针地址
0x7ffedfc920d8 = copy的对象指针地址
0x600003af3cc0 = 原对象的内存地址 - bbb
0xd6bb373121e18d02 = copy的对象的内存地址 - bbb
从打印结果我们可以看出,对可变对象进行
copy
后
- 指向内存地址的指针地址不一样
- 内存地址不一样,开辟了新的内存地址(存放的内容都是
bbb
)所以这是一个
深拷贝
-
例子四:对可变对象B进行mutableCopy
NSMutableString *B = [NSMutableString stringWithString:@"bbb"];
id C = [B mutableCopy];
NSLog(@"\n可变-mutableCopy\n%p = 原对象指针地址\n%p = copy的对象指针地址\n%p = 原对象的内存地址 - %@\n%p = copy的对象的内存地址 - %@",&B,&C,B,B,C,C);
控制台打印
2020-06-03 17:54:34.595064+0800 Demo[12717:882756]
可变-mutableCopy
0x7ffee78cf0e0 = 原对象指针地址
0x7ffee78cf0d8 = copy的对象指针地址
0x600001143690 = 原对象的内存地址 - bbb
0x600001143660 = copy的对象的内存地址 - bbb
从打印结果我们可以看出,对不可变对象进行
mutableCopy
后
- 指向内存地址的指针地址不一样
- 内存地址不一样,开辟了新的内存地址(存放的内容都是
bbb
)所以这是一个
深拷贝
总结
[immutableObject copy]; //浅拷贝,拷贝后的对象不可变
[immutableObject mutableCopy]; //深拷贝,拷贝后的对象可变
[mutableObject copy]; //深拷贝,拷贝后的对象不可变(不安全,不建议用)
[mutableObject mutableCopy]; //深拷贝,拷贝后的对象可变
通过四种不同情况的代码测试,最终得到的结论是
- 如果对象是不可变的,
copy
会进行浅拷贝,mutableCopy
会进行深拷贝 - 如果对象是可变的,那么无论是
copy
还是mutableCopy
都会进行深拷贝 - 如果是
copy
,拷贝后的对象不可变 - 如果是
mutableCopy
,拷贝后的对象可变
拓展 - 面试题:修饰属性时用strong
还是copy
?
最安全的做法(推荐):修饰不可变对象(NSString
,NSArray
,NSDictionary
等)用copy
,修饰可变对象(NSMutableString
,NSMutableArray
,NSMutableDictionary
等)用strong
- Why?
我们用字符串(NSString
&&NSMutableString
)来做测试 - 定义2个NSString类型的属性,分别用strong和copy修饰
@property (nonatomic,strong) NSString *strStrong;
@property (nonatomic,copy) NSString *strCopy;
测试一:strong修饰,赋值不可变
- 创建一个不可变的局部变量
immutableStr
- 让
strStrong
指向immutableStr
所指向的内存地址 - 再将
immutableStr
指向新的内存地址
NSString *immutableStr = @"aaa";
self.strStrong = immutableStr;
immutableStr = @"bbb";
经过LLDB调试可以看出,运行过了前两行代码,immutableStr
和strStrong
指向同一片内存地址,但是他们的指针变量是不一样的
//运行完前两句代码
//NSString *immutableStr = @"aaa";
//self.strStrong = immutableStr;
//指向的内存地址相同,都是0x000000010bb3d338
(lldb) p immutableStr
(__NSCFConstantString *) $0 = 0x000000010bb3d338 @"aaa"
(lldb) p _strStrong
(__NSCFConstantString *) $1 = 0x000000010bb3d338 @"aaa"
//指针地址不同,0x00007ffee40c70e8和0x00007fd7194098b8
(lldb) p &immutableStr
(NSString **) $2 = 0x00007ffee40c70e8
(lldb) p &_strStrong
(NSString **) $3 = 0x00007fd7194098b8
//运行完第三句代码
//immutableStr = @"bbb";
(lldb) p immutableStr
(__NSCFConstantString *) $4 = 0x000000010bb3d358 @"bbb"
(lldb) p &immutableStr
(NSString **) $5 = 0x00007ffee40c70e8
(lldb) p _strStrong
(__NSCFConstantString *) $6 = 0x000000010bb3d338 @"aaa"
(lldb) p &_strStrong
(NSString **) $7 = 0x00007fd7194098b8
第三句代码的意思是让immutableStr
指针变量指向存放@"bbb"
的新的内存地址,而且这个操作并不影响strStrong
,因为只是改变了immutableStr的指针指向,所以再次打印strStrong
的指针地址和内存地址还是跟原来一样,没有变化。
tips:immutableStr是不可变对象,不能改变所指向内存地址的值,只能重新生成一块新的内存地址并指向它。
测试二:strong修饰,赋值可变
- 创建一个可变的局部变量
mutableStr
- 让
strStrong
指向mutableStr
所指向的内存地址 - 再给
mutableStr
追加字符串
NSMutableString *mutableStr = [NSMutableString stringWithString:@"aaa"];
self.strStrong = mutableStr;
[mutableStr appendString:@"bbb"];
经过LLDB调试可以看出,运行过了前两行代码,mutableStr
和strStrong
指向同一片内存地址,但是他们的指针变量是不一样的
//运行完前两句代码
//NSMutableString *mutableStr = [NSMutableString stringWithString:@"aaa"];
//self.strStrong = mutableStr;
(lldb) p mutableStr
(__NSCFString *) $0 = 0x00006000027ddc20 @"aaa"
(lldb) p _strStrong
(__NSCFString *) $1 = 0x00006000027ddc20 @"aaa"
(lldb) p &mutableStr
(NSMutableString **) $2 = 0x00007ffeee7460e8
(lldb) p &_strStrong
(NSString **) $3 = 0x00007f871e40b658
//运行完第三句代码
//[mutableStr appendString:@"bbb"];
(lldb) p mutableStr
(__NSCFString *) $4 = 0x00006000027ddc20 @"aaabbb"
(lldb) p _strStrong
(__NSCFString *) $5 = 0x00006000027ddc20 @"aaabbb"
(lldb) p &mutableStr
(NSMutableString **) $6 = 0x00007ffeee7460e8
(lldb) p &_strStrong
(NSString **) $7 = 0x00007f871e40b658
- 可以发现,运行完第三句代码后,
immutableStr
和strStrong
的值都变成了@"aaabbb"
- 看到这里,你可能会有疑问,我只是给
immutableStr
追加了字符串@“bbb”
,为什么strStrong
也会跟着变成@"aaabbb"
了呢?
解释:由于mutableStr是可变对象,所以它可以在
mutableStr
指向的那片内存的字符串后面追加字符串@"bbb"
,那么这个问题就来了,这个操作实际上已经改变了原来的那片内存地址的值,那么会出现什么问题呢?没错,相当于strStrong
所指向的内存地址的值也被修改了,所以strStrong
对应内存中的值也变成了@"aaabbb"
。
如果你对指针和内存管理不熟悉的话,很容易会遇到这种难以发现的bug,因为站在你的角度来看,你只是改变了A
,不会影响B
,但实际上B
已经被改变了。
那么如何防止这种问题的产生?
答:使用copy
修饰
我们再来测试一下用copy
修饰后的字符串,不可变的赋值我们就不测试了,因为原理跟上面的一样,我们来测试一下可变的赋值
测试三:copy
修饰,赋值可变
- 创建一个可变的局部变量
mutableStr
- 让
strCopy
指向mutableStr
所指向的内存地址 - 再给
mutableStr
追加字符串
NSMutableString *mutableStr = [NSMutableString stringWithString:@"aaa"];
self.strCopy = mutableStr;
[mutableStr appendString:@"bbb"];
经过LLDB调试可以看出,运行过了前两行代码,mutableStr
和strCopy
指向了不同的内存地址,指针变量也是不一样的
//运行完前两句代码
//NSMutableString *mutableStr = [NSMutableString stringWithString:@"aaa"];
//self.strCopy = mutableStr;
(lldb) p mutableStr
(__NSCFString *) $0 = 0x000060000271c0f0 @"aaa"
(lldb) p _strCopy
(NSTaggedPointerString *) $1 = 0x9c1cf86b18404957 @"aaa"
(lldb) p &mutableStr
(NSMutableString **) $2 = 0x00007ffee338a0e8
(lldb) p &_strCopy
(NSString **) $3 = 0x00007ff821d0a320
//运行完第三句代码
//[mutableStr appendString:@"bbb"];
(lldb) p mutableStr
(__NSCFString *) $4 = 0x000060000271c0f0 @"aaabbb"
(lldb) p _strCopy
(NSTaggedPointerString *) $5 = 0x9c1cf86b18404957 @"aaa"
(lldb) p &mutableStr
(NSMutableString **) $6 = 0x00007ffee338a0e8
(lldb) p &_strCopy
(NSString **) $7 = 0x00007ff821d0a320
- 当给
mutableStr
追加完字符串之后,可以看出只有mutableStr
改变了内存中的值,而strCopy
仍然是@"aaa"
- 这是因为用
copy
修饰strCopy
属性,底层默认会在strCopy
的setter
方法中对所赋值的可变字符串mutableStr
先做一次copy
操作然后再赋值,那么我们很容易能推出来这是一次深拷贝,会开辟新的内存地址,那么使得strCopy
会指向新的内存地址,存放copy
后的值- 那么就能得出
mutableStr
追加字符串并不会影响strCopy
了,因为现在他们是两个不同的指针分别指向不同的内存地址
如果你已经看完这么详细(啰嗦:D)的一个分析,相信上面那个面试题就可以很好的回答出来了。
参考文章
面试题分解—「浅复制/深复制、定义属性使用copy还是strong ?