Objective-C中copy与mutableCopy问题

先说下概念,我们对变量的复制,其实就是在写代码的过程中,再定义多几个不同名字的变量,让他们都“等于”某一个变量,这个过程我认为就是我们平常说的“复制”。
基本数据类型 对于基本数据类型,如int,double,BOOL这些,在赋值的过程中就是真正意义上的复制了,赋值时不仅把值传递到新的变量中,而且新的变量也 重新开辟了内存 ,使得原来的变量和后来的变量所指的不是同一块内存,就如同现实中真的复制(克隆)了一个新的一模一样个体一样。于是,我们把这种有 新开辟内存 的复制,暂且先叫做 深复制(深复制也有分两种,后面会说到)。

  • 实验代码:
int a = 0; int b = a; JMPLog(&a); JMPLog(&b);

  • 运行结果:
0x7ffee662c1dc 0x7ffee662c1d8

结论1:所有基本数据类型的复制,都是深复制 非集合类型对象 所谓非集合类型对象,比较常用的就是NSString,下面就以NSString作为例子,说明copy和mutableCopy之间的区别,并与深复制浅复制进行联系。
首先展示一个错误的示范,网上有很多关于copy和mutableCopy的文章,里面举了这么一个例子:
NSString *str1 = @"str1"; NSString *str2 = [str1 copy]; str1 = @"asdf"; NSLog(@"\nstr1 = %@ str1P = %p \n str2 = %@ str2P = %p", str1, str1, str2, str2); /*输出结果,修改str2 同理 str1 = asdf str1P = 0x10776b1a0 str2 = str1 str2P = 0x10776b180 */

然后就说,因为str2 = str1的时候,两个字符串都是不可变的,指向的同一块内存空间中的 @"str1",是不可能变成@"abcd"的。所以这个时候,为了优化性能,系统没必要另外提供内存,只生成另外一个指针,指向同一块内存空间就行。
但是当你从新给 str1 或者str2赋值的时候,因为之前的内容不可变,还有互不影响的原则下,这个时候,系统会从新开辟一块内存空间。
上面的解释和代码有个很严重的问题,当执行str1 = @"asdf"; 这行代码的时候其实str1的指针已经指向了新的字符串@“asdf”身上了,所以这并不能很好的说明深浅复制的问题。
言归正传,我们先讨论对于不可变的非集合类型对象(这里用NSString作为例子),当发送copy和mutableCopy消息后,新的对象的内存情况。
  • 实验代码:
NSString *strA = @"strA"; NSString *strB = [strA copy]; NSString *strC = [strA mutableCopy]; NSLog(@"Value -- strA: %@, strB: %@, strC: %@", strA, strB, strC); NSLog(@"Pointer -- strA: %p, strB: %p, strC: %p", &strA, &strB, &strC); NSLog(@"Pointer of value -- strA: %p, strB: %p, strC: %p", strA, strB, strC);

  • 运行结果:
Value -- strA: strA, strB: strA, strC: strA Pointer -- strA: 0x7ffee3adf1d8, strB: 0x7ffee3adf1d0, strC: 0x7ffee3adf1c8 Pointer of value -- strA: 0x10c124800, strB: 0x10c124800, strC: 0x6000009e0a80

结果分析:
  1. 可以看出,三个字符串的内容都是一样的,达到了我们对“复制”这个概念的目的。但是,字符串B和C是不是做到真正意义上的复制呢?我们要看B,C变量所指向的内存地址(Pointer of value),结果显示B的地址与A的地址一致(0x1039f0800),而C的地址则与A的不同(A: 0x1039f0800, C: 0x6000021f4450),所以只有C才是真正意义上的复制,也就是我们上面提到的 深复制 ,而像字符串B这种 只是把指针指向同一块内存地址,而实现对应内容“复制” 的做法,我们称之为 浅复制
  2. 观察三个字符串变量本身的地址(注意不是字符串所指向的地址),发现三个是不同的,也就是说上述行为是新建了三个指针(指针本身也占有内存),然后A,B指向的是同一块地址,C则指向另一块新的地址,这些内存地址的内容都是@“strA”。
    就目前来看,我们暂时可以得出的结论是,copy的作用仅仅是把指针指向同一块内存地址,是浅复制,那么如果有其他手段能够改变该段内存的内容,那么用copy消息返回的对象的值(所指内存的内容)也会跟着原本被“复制”的对象而改变。而mutableCopy的作用则会新开辟一段内存,让对象指向该段内存,从而实现复制,如果被复制的对象内容改变,新对象的内容并不会跟着改变(由于指向的不是同一段内存)。
接下来再讨论可变非集合类型的对象(这里用NSMutableString作为例子),先上代码
  • 实验代码:
NSMutableString *strA = [NSMutableString stringWithFormat:@"strA"]; NSString *copyStr = [strA copy]; NSString *copyStr2 = [strA copy]; NSMutableString *copyMStr = [strA copy]; NSMutableString *copyMStr2 = [strA copy]; NSString *mutableCopyStr = [strA mutableCopy]; NSString *mutableCopyStr2 = [strA mutableCopy]; NSMutableString *mutableCopyMStr = [strA mutableCopy]; NSMutableString *mutableCopyMStr2 = [strA mutableCopy]; NSLog(@"strA -- %p &strA -- %p", strA, &strA); NSLog(@"copyStr -- %p ©Str -- %p", copyStr, ©Str); NSLog(@"copyStr2 -- %p ©Str2 -- %p", copyStr2, ©Str2); NSLog(@"copyMStr -- %p ©MStr -- %p", copyMStr, ©MStr); NSLog(@"copyMStr2 -- %p ©MStr2 -- %p", copyMStr2, ©MStr2); NSLog(@"mutableCopyStr -- %p &mutableCopyStr -- %p", mutableCopyStr, &mutableCopyStr); NSLog(@"mutableCopyStr2 -- %p &mutableCopyStr2 -- %p", mutableCopyStr2, &mutableCopyStr2); NSLog(@"mutableCopyMStr -- %p &mutableCopyMStr -- %p", mutableCopyMStr, &mutableCopyMStr); NSLog(@"mutableCopyMStr2 -- %p &mutableCopyMStr2 -- %p", mutableCopyMStr2, &mutableCopyMStr2);

  • 运行结果:
strA -- 0x600001c093e0 &strA -- 0x7ffeebd531d8 copyStr -- 0xd4d03e2a99e5492a ©Str -- 0x7ffeebd531d0 copyStr2 -- 0xd4d03e2a99e5492a ©Str2 -- 0x7ffeebd531c8 copyMStr -- 0xd4d03e2a99e5492a ©MStr -- 0x7ffeebd531c0 copyMStr2 -- 0xd4d03e2a99e5492a ©MStr2 -- 0x7ffeebd531b8 mutableCopyStr -- 0x600001c08930 &mutableCopyStr -- 0x7ffeebd531b0 mutableCopyStr2 -- 0x600001c08a50 &mutableCopyStr2 -- 0x7ffeebd531a8 mutableCopyMStr -- 0x600001c08f30 &mutableCopyMStr -- 0x7ffeebd531a0 mutableCopyMStr2 -- 0x600001c09290 &mutableCopyMStr2 -- 0x7ffeebd53198

结果分析:
  • 同样,观察每个指针自身的地址各不相同,说明也是生成了各个不同的指针,符合逻辑。观察使用copy消息的4个变量,发现无论是NSString还是NSMutableString,只要是copy消息返回的都是与原字符串A不同指向的地址。但是返回的这些指针,都是指向同一块新的内存地址(0xd4d03e2a99e5492a)。当字符串A是可变字符串,copy消息返回了新的对象,开辟了新的内存,这些对象都会指向新开辟的这一段内存。根据前面描述的,这种复制应该是深复制,这与上一个例子的非可变类型有所区别。
  • 对于mutableCopy,与copy的情况有类似的地方,都是返回的是与原字符串A不同指向的地址。不同的是,这些指针指向的却是不是同一块内存地址,无论我们定义的是NSString或者是NSMutableString,每个新的对象都是指向一块全新的地址。不过我们目前只考虑是否开辟了新的内存,所以这种情况也是认为是深复制。
结论2:
- 非可变非集合类型 可变非集合类型
copy 浅复制 深复制
mutableCopy 深复制 深复制
集合类型对象 集合类型对象应该是我们开发过程中最常用到的结构之一,比如NSArray,NSDictionary等。那么对于集合类型的对象,我们向他们发送copy和mutableCopy消息时,又会产生何种效果?
首先还是讨论不可变的情况,这里以NSDictionary作为例子(选择字典作为例子是想更全面的研究字典中的Key和Value出现的情况是否相同,数组则体现不出这个效果)。
  • 实验代码:
NSDictionary *aDic = @{@"aaa": @"111"}; NSDictionary *copyDic = [aDic copy]; NSDictionary *copyDic2 = [aDic copy]; NSMutableDictionary *copyMDic = [aDic copy]; NSMutableDictionary *copyMDic2 = [aDic copy]; NSDictionary *mutableCopyDic = [aDic mutableCopy]; NSDictionary *mutableCopyDic2 = [aDic mutableCopy]; NSMutableDictionary *mutableCopyMDic = [aDic mutableCopy]; NSMutableDictionary *mutableCopyMDic2 = [aDic mutableCopy]; NSLog(@"aDic -- %p aDic[@\"aaa\"] -- %p key aaa -- %p", aDic, aDic[@"aaa"], [[aDic allKeys] firstObject]); NSLog(@"copyDic -- %p copyDic[@\"aaa\"] -- %p key aaa -- %p", copyDic, copyDic[@"aaa"], [[copyDic allKeys] firstObject]); NSLog(@"copyDic2 -- %p copyDic2[@\"aaa\"] -- %p key aaa -- %p", copyDic2, copyDic2[@"aaa"], [[copyDic2 allKeys] firstObject]); NSLog(@"copyMDic -- %p copyMDic[@\"aaa\"] -- %p key aaa -- %p", copyMDic, copyMDic[@"aaa"], [[copyMDic allKeys] firstObject]); NSLog(@"copyMDic2 -- %p copyMDic2[@\"aaa\"] -- %p key aaa -- %p", copyMDic2, copyMDic2[@"aaa"], [[copyMDic2 allKeys] firstObject]); NSLog(@"mutableCopyDic -- %p mutableCopyDic[@\"aaa\"] -- %p key aaa -- %p", mutableCopyDic, mutableCopyDic[@"aaa"], [[mutableCopyDic allKeys] firstObject]); NSLog(@"mutableCopyDic2 -- %p mutableCopyDic2[@\"aaa\"] -- %p key aaa -- %p", mutableCopyDic2, mutableCopyDic2[@"aaa"], [[mutableCopyDic2 allKeys] firstObject]); NSLog(@"mutableCopyMDic -- %p mutableCopyMDic[@\"aaa\"] -- %p key aaa -- %p", mutableCopyMDic, mutableCopyMDic[@"aaa"], [[mutableCopyMDic allKeys] firstObject]); NSLog(@"mutableCopyMDic2 -- %p mutableCopyMDic2[@\"aaa\"] -- %p key aaa -- %p", mutableCopyMDic2, mutableCopyMDic2[@"aaa"], [[mutableCopyMDic2 allKeys] firstObject]);

  • 运行结果:
aDic -- 0x6000027a1e00 aDic[@"aaa"] -- 0x1042da820 key aaa -- 0x1042da800 copyDic -- 0x6000027a1e00 copyDic[@"aaa"] -- 0x1042da820 key aaa -- 0x1042da800 copyDic2 -- 0x6000027a1e00 copyDic2[@"aaa"] -- 0x1042da820 key aaa -- 0x1042da800 copyMDic -- 0x6000027a1e00 copyMDic[@"aaa"] -- 0x1042da820 key aaa -- 0x1042da800 copyMDic2 -- 0x6000027a1e00 copyMDic2[@"aaa"] -- 0x1042da820 key aaa -- 0x1042da800 mutableCopyDic -- 0x6000027a16e0 mutableCopyDic[@"aaa"] -- 0x1042da820 key aaa -- 0x1042da800 mutableCopyDic2 -- 0x6000027a1d40 mutableCopyDic2[@"aaa"] -- 0x1042da820 key aaa -- 0x1042da800 mutableCopyMDic -- 0x6000027a1920 mutableCopyMDic[@"aaa"] -- 0x1042da820 key aaa -- 0x1042da800 mutableCopyMDic2 -- 0x6000027a0ee0 mutableCopyMDic2[@"aaa"] -- 0x1042da820 key aaa -- 0x1042da800

结果分析:
  • 情况和非集合类型相似,对于被复制的对象是不可变集合类型,当向对象发送copy消息时,无论新定义的对象是可变还是不可变的,返回的总是和原对象所指向的地址一样的地址,同时,字典里无论是key还是value都是和原对象是一致的。这还是比较符合认知,既然所有的指针都指向同一个地址,证明这里copy消息只是做了一个浅复制,既然是浅复制,本质上并没有真正“复制”一个新的对象出来(没有开辟新的内存地址),而是只是简单地把新的对象指向了原来对象的地址,所以没有新的字典和内容生成,故所有地址都是一致的。
  • 当向对象发送mutableCopy消息时,无论新定义的对象是可变还是不可变的,返回的总是一段全新的内存,指向全新的地址,不过有趣的是,字典里的key和value也是像前面copy的情况一样,都是和原对象是一致。这就有点奇妙了,讨论到现在,对于复制我们讨论到的就两种复制,浅复制和深复制,区分它们的方法就是看是否有开辟新的内存,但现在的情况是,mutableCopy确实是返回了新开辟内存的新的字典对象,但字典里面的内容却和原对象的是指向同一块地址,也就是说如果通过某些手段改变了这些地址的值,所有这些新的字典对象的key和value也会随之改变。这还是跟我们理解中的复制有点区别。所以这里回应到一开始提到的深复制也有两种情况,一种是单单给对象开辟新的内存,另一种是 不仅给新的对象开辟内存,而且会对其里面包含的内容开辟新的地址,彻底复制一份全新的独立的拷贝 ,我们把后者这种深复制叫做 两层深复制 (two-layer-copy),同时为了区分,现在我们把第一种的深复制叫做 单层深复制
接下来把可变集合类型也测试一下。
  • 实验代码
NSMutableDictionary *aDic = [NSMutableDictionary dictionaryWithCapacity:10]; [aDic setObject:@"111" forKey:@"aaa"]; NSDictionary *copyDic = [aDic copy]; NSDictionary *copyDic2 = [aDic copy]; NSMutableDictionary *copyMDic = [aDic copy]; NSMutableDictionary *copyMDic2 = [aDic copy]; NSDictionary *mutableCopyDic = [aDic mutableCopy]; NSDictionary *mutableCopyDic2 = [aDic mutableCopy]; NSMutableDictionary *mutableCopyMDic = [aDic mutableCopy]; NSMutableDictionary *mutableCopyMDic2 = [aDic mutableCopy]; NSLog(@"aDic -- %p aDic[@\"aaa\"] -- %p key aaa -- %p", aDic, aDic[@"aaa"], [[aDic allKeys] firstObject]); NSLog(@"copyDic -- %p copyDic[@\"aaa\"] -- %p key aaa -- %p", copyDic, copyDic[@"aaa"], [[copyDic allKeys] firstObject]); NSLog(@"copyDic2 -- %p copyDic2[@\"aaa\"] -- %p key aaa -- %p", copyDic2, copyDic2[@"aaa"], [[copyDic2 allKeys] firstObject]); NSLog(@"copyMDic -- %p copyMDic[@\"aaa\"] -- %p key aaa -- %p", copyMDic, copyMDic[@"aaa"], [[copyMDic allKeys] firstObject]); NSLog(@"copyMDic2 -- %p copyMDic2[@\"aaa\"] -- %p key aaa -- %p", copyMDic2, copyMDic2[@"aaa"], [[copyMDic2 allKeys] firstObject]); NSLog(@"mutableCopyDic -- %p mutableCopyDic[@\"aaa\"] -- %p key aaa -- %p", mutableCopyDic, mutableCopyDic[@"aaa"], [[mutableCopyDic allKeys] firstObject]); NSLog(@"mutableCopyDic2 -- %p mutableCopyDic2[@\"aaa\"] -- %p key aaa -- %p", mutableCopyDic2, mutableCopyDic2[@"aaa"], [[mutableCopyDic2 allKeys] firstObject]); NSLog(@"mutableCopyMDic -- %p mutableCopyMDic[@\"aaa\"] -- %p key aaa -- %p", mutableCopyMDic, mutableCopyMDic[@"aaa"], [[mutableCopyMDic allKeys] firstObject]); NSLog(@"mutableCopyMDic2 -- %p mutableCopyMDic2[@\"aaa\"] -- %p key aaa -- %p", mutableCopyMDic2, mutableCopyMDic2[@"aaa"], [[mutableCopyMDic2 allKeys] firstObject]);

  • 运行结果:
aDic -- 0x600002f53240 aDic[@"aaa"] -- 0x10709f800 key aaa -- 0x10709f820 copyDic -- 0x600002f53220 copyDic[@"aaa"] -- 0x10709f800 key aaa -- 0x10709f820 copyDic2 -- 0x600002f531a0 copyDic2[@"aaa"] -- 0x10709f800 key aaa -- 0x10709f820 copyMDic -- 0x600002f52d20 copyMDic[@"aaa"] -- 0x10709f800 key aaa -- 0x10709f820 copyMDic2 -- 0x600002f52f00 copyMDic2[@"aaa"] -- 0x10709f800 key aaa -- 0x10709f820 mutableCopyDic -- 0x600002f52de0 mutableCopyDic[@"aaa"] -- 0x10709f800 key aaa -- 0x10709f820 mutableCopyDic2 -- 0x600002f52d60 mutableCopyDic2[@"aaa"] -- 0x10709f800 key aaa -- 0x10709f820 mutableCopyMDic -- 0x600002f52e00 mutableCopyMDic[@"aaa"] -- 0x10709f800 key aaa -- 0x10709f820 mutableCopyMDic2 -- 0x600002f52e60 mutableCopyMDic2[@"aaa"] -- 0x10709f800 key aaa -- 0x10709f820

结果分析:
  • 情况有点出乎我预料,本来以为是跟非集合类型(NSString)类似,用copy消息返回的是一段新开辟的内存(深复制),所有4个指针都是指向那段新开的内存。但现实的情况确实每个对象各自指向了不同的新的内存,尽管都是深复制。而且这里提到的深复制,是我们上述的单层深复制,因为可以看出字典的Key和value都是跟原来的字典一样的。
  • 同时,mutableCopy就和之前的完全一致了,每个对象都各自开辟了新的互不相同的内存,然而字典内容也还是和原字典的一致,同样也是单程深复制。
结论3:
- 不可变集合类型 可变集合类型
copy 浅复制 单层深复制
mutableCopy 单层深复制 单层深复制
彩蛋:在一开始的时候,我是把key和value都设置成@“aaa”,运行后发现key和value的地址都是一样的,这不就是浅复制吗?猜测系统这样的做法是为了节省内存吧?
最终结论 所以,我们可以得出:
对于不可变的非集合类对象进行 copy 操作,其内存地址并没有发生变化,属于浅复制;进行 mutableCopy 操作,内存地址发生了变化,深复制。
【Objective-C中copy与mutableCopy问题】对于不可变的集合类对象进行 copy 操作,其内存地址并没有发生变化,属于浅复制;进行 mutableCopy 操作,内存地址发生了变化,但是其中的内容的内存地址并没有发生变化,属于单层深复制。
对于可变集合类对象,不管是进行 copy 操作还是 mutableCopy 操作,其内存地址都发生了变化,但是其中的元素内存地址都没有发生变化,属于单层深复制。

    推荐阅读