iOS-浅拷贝与深拷贝

当你的才华撑不起你的野心时,你就应该静下来学习。 —— CJJ

Why Learn?

浅拷贝和深拷贝是必须要掌握的知识点,工作中也会频繁用到,所以在这里记录总结一下自己的理解,方便日后查看。

Ask?

如果你觉得浅拷贝就是copy,深拷贝就是mutableCopy的话,那就大错特错了,请继续往下看

定义

首先浅拷贝是什么?深拷贝又是什么呢?

这两种都是拷贝(复制)对象的意思,不同的是

  • 浅拷贝在拷贝对象的过程中
    • 拷贝了指向对象内存地址的指针
    • 拷贝后新的指针仍然指向原来的内存地址
  • 深拷贝在拷贝对象的过程中
    • 不仅拷贝了指向对象内存地址的指针
    • 还会拷贝原指针所指向的内存地址所存储的内容
    • 并且在堆中重新生成一块内存地址来存放所拷贝的内容
    • 然后新的指针会指向新的内存地址

简单对比

浅拷贝:拷贝指针,不开辟内存地址
深拷贝:拷贝指针和内容,开辟内存地址(存放拷贝的内容)

如何判断?

要判断一个拷贝是浅拷贝还是深拷贝,不仅看copy还是mutableCopy,还要考虑拷贝的对象是可变的还是不可变的

举例说明,假如存在不可变对象A可变对象B,对AB分别进行copymutableCopy,结果如下

  • 例子一:对不可变对象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?

最安全的做法(推荐):修饰不可变对象(NSStringNSArrayNSDictionary等)用copy,修饰可变对象(NSMutableStringNSMutableArrayNSMutableDictionary等)用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调试可以看出,运行过了前两行代码,immutableStrstrStrong指向同一片内存地址,但是他们的指针变量是不一样的

//运行完前两句代码
//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调试可以看出,运行过了前两行代码,mutableStrstrStrong指向同一片内存地址,但是他们的指针变量是不一样的

//运行完前两句代码
//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
  • 可以发现,运行完第三句代码后,immutableStrstrStrong的值都变成了@"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调试可以看出,运行过了前两行代码,mutableStrstrCopy指向了不同的内存地址,指针变量也是不一样的

//运行完前两句代码
//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属性,底层默认会在strCopysetter方法中对所赋值的可变字符串mutableStr先做一次copy操作然后再赋值,那么我们很容易能推出来这是一次深拷贝,会开辟新的内存地址,那么使得strCopy会指向新的内存地址,存放copy后的值
  • 那么就能得出mutableStr追加字符串并不会影响strCopy了,因为现在他们是两个不同的指针分别指向不同的内存地址

如果你已经看完这么详细(啰嗦:D)的一个分析,相信上面那个面试题就可以很好的回答出来了。

参考文章
面试题分解—「浅复制/深复制、定义属性使用copy还是strong ?

你可能感兴趣的:(iOS-浅拷贝与深拷贝)