iOS中的Copy和mutableCopy

拷贝:顾名思义就是将一个对象复制一份出来。说到iOS中得拷贝操作,大概用的最多的应该就是数组的拷贝操作。如果自己的类想支持拷贝操作,那就要实现NSCopy协议。

- (id)copyWithZone:(NSZone *)zone;

与之对应的还有一个可变版本拷贝操作,需要实现NSMutableCopying协议。

- (id)mutableCopyWithZone:(nullable NSZone *)zone;

自定义这种网上还是比较多,这里主要介绍字符串,数组,字典的Copy和mutableCopy操作,以及衍伸出的浅拷贝和深拷贝的问题。

1. NSString

大家一般申明字符串属性的时候都会用这种方式

@property (nonatomic, copy) NSString *str;

当然也会有这种

@property (nonatomic, strong) NSString *str;

那到底应该使用哪一种比较好,还是说两者都可以使用呢,接下来将进一步来分析他们。
当我们给字符串属性赋值的时候,实际调用的是set方法:

首先我们来分析strong这种方式:

- (void)setStr:(NSString *)str {
    _str = str;
}

�接下来测试一下:

NSString *a = @"One";
self.str = a;
NSLog(@"变量a所指向的地址--%p,内容--%@",a,a);
NSLog(@"self.str所指向的地址--%p,内容--%@",self.str,self.str);

 变量a所指向的地址--0x10a91c088,内容--One
 self.str所指向的地址--0x10a91c088,内容--One

如上述代码可以看到,将变量a赋值给self.str的时候,其实就是让self.str指向了变量a所指向的地址,也就是字符串One在堆中得地址。所以self.str和变量a指向的是同一个地址。

//给a重新赋值
a = @"Two";
NSLog(@"变量a所指向的地址--%p,内容--%@",a,a);
NSLog(@"self.str所指向的地址--%p,内容--%@",self.str,self.str);
变量a所指向的地址--0x10a91c088,内容--Two
self.str所指向的地址--0x10a91c0e8,内容--One

当我们给变量a重新赋值的时候,其实是让变量a重新指向了新的地址,也就是字符串Two在堆中的地址,但是self.str并不会改变。无论变量a怎么赋值,都不会影响到self.str,这也是我们所希望看到的。

//定义一个可变的字符串a
NSMutableString *a = [NSMutableString stringWithString:@"One"];
self.str = a;
NSLog(@"变量b所指向的地址--%p,内容--%@",a,a); 
NSLog(@"self.str所指向的地址--%p",self.str);

//a中追加了字符串123
[a appendString:@"123"];
NSLog(@"变量b所指向的地址--%p,内容--%@",a,a);
NSLog(@"self.str所指向的地址--%p,内容--%@",self.str,self.str);

变量a所指向的地址--0x7fc51bca40e0,内容--One
self.str所指向的地址--0x7fc51bca40e0,内容--One

//a中追加了字符串123
变量b所指向的地址--0x7fc51bca40e0,内容--One123
self.str所指向的地址--0x7fc51bca40e0,内容--One123

上述代码中,我们声明了一个可变字符串变量a,并赋值one。当我们在变量a中追加了字符串123之后,self.str的内容也随之改变了。因为变量a和self.str所指向的地址是同一个。当我们给a追加字符串的时候,并没有重新让a指向新的地址。所以当我们给其中一个追加内容的时候,另一个也会随之改变。假如我们在某个场景中又用到了变量a,并给变量a追加了一些新的内容,就会导致self.str的内容也被改变了

接下来我们分析copy

- (void)setStr:(NSString *)str {
    _str = [str copy];
}

给字符串赋值的时候,在其内部使用拷贝的操作。我们按照之前的方式进行测试。

NSString *a = @"One";
self.str = a;
NSLog(@"变量a所指向的地址--%p,内容--%@",a,a);
NSLog(@"self.str所指向的地址--%p,内容--%@",self.str,self.str);

a = @"Two";
NSLog(@"变量a所指向的地址--%p,内容--%@",a,a);
NSLog(@"self.str所指向的地址--%p,内容--%@",self.str,self.str);

变量a所指向的地址--0x104aad088,内容--One
self.str所指向的地址--0x104aad088,内容--One

变量a所指向的地址--0x104aad0e8,内容--Two
self.str所指向的地址--0x104aad088,内容--One

从上面的代码可以看出,结果完全是和strong是一样的。但是内部是有细微差别的。再其内部的set方法中 str = [a copy] 进行了一份浅拷贝,也就是所谓的指针拷贝,使str指向了变量a所指向的地址。

接下来我们将变量a定义成可变字符串

NSMutableString *a = [NSMutableString stringWithString:@"One"];
self.str = a;
NSLog(@"变量b所指向的地址--%p",a);
NSLog(@"self.str所指向的地址--%p",self.str);
//a中追加了字符串123
[a appendString:@"123"];
NSLog(@"变量b所指向的地址--%p,内容--%@",a,a);
NSLog(@"self.str所指向的地址--%p,内容--%@",self.str,self.str);

变量a所指向的地址--0x7fc4f940fbe0
self.str所指向的地址--0xa00000000656e4f3
//a中追加了字符串123
变量a所指向的地址--0x7fc4f940fbe0,内容--One123
self.str所指向的地址--0xa00000000656e4f3,内容--One

大家是不是发现不一样的地方了?当我们在变量a中追击加了字符串之后,self.str并没有改变其内容。也就是说self.str并不会受到变量a影响。
我们仔细观察上面输出的地址。当把变量a赋值给self.str之后,打印他们的地址,我们发现self.str指向的地址不是变量a所指向的地址。这是因为当我们把变量a赋值给self.str时候,再其内部进行了一次深拷贝,也就是把整个对象进行了拷贝。
set方法内部

str = [a copy];
因为我们变量a是一个可变字符串,可变版本进行copy执行,会产生一个不可变的版本(NSString),此时是一个深拷贝,在堆中重新开辟一个新的内存,里面存放了相同的内容One。str指向了这个新的内存地址,只是这个新对象是一个不可变的版本。所以我们看到两次输出的地址是完全不一样的。
当我们接下来给变量a追加字符串的时候,也就不会影响到self.str中的内容了。

所以定义字符串属性的时候建议使用copy关键字

2.字符串中Copy和mutableCopy操作

NSString *str = @"123";
NSString *copyStr = [str copy];
NSLog(@"str所指向的地址--%p",str);
NSLog(@"copyStr所指向的地址--%p",copyStr);

str所指向的地址--0x105e40088
copyStr所指向的地址--0x105e40088

上面的代码可以看出,当一个不可变的字符串进行copy之后返回的还是一个不可变的字符串,并且是一个浅拷贝(指针拷贝),他们还是都指向同一个地址。

NSString *str = @"123";
NSMutableString *mutableCopyStr = [str mutableCopy];
NSLog(@"str所指向的地址--%p",str);
NSLog(@"mutableCopyStr所指向的地址--%p",mutableCopyStr);

str所指向的地址--0x102859088
mutableCopyStr所指向的地址--0x7fade2e3b170

上面的代码可以看出,当一个不可变字符串进行mutableCopy之后返回一个可变的对象,进行了一次深拷贝,他们分别指向了不同的地址,只是内容还是一样的。

NSMutableString *str = [NSMutableString stringWithString:@"123"];
NSMutableString *copyStr = [str copy];
NSLog(@"str所指向的地址--%p,内容--%@",str,str);
NSLog(@"copyStr所指向的地址--%p,内容--%@",copyStr,copyStr);

str所指向的地址--0x7fcac163fc30,内容--123
copyStr所指向的地址--0xa000000003332313,内容--123

上面的代码可以看出,当一个可变字符串进行copy之后返回了一个不可变的对象,进行了一次深拷贝,他们分别指向了不同的地址,只是内容还是一样的。

NSMutableString *str = [NSMutableString stringWithString:@"123"];
NSMutableString *mutableCopyStr = [str mutableCopy];
NSLog(@"str所指向的地址--%p,内容--%@",str,str);
NSLog(@"mutableCopyStr所指向的地址--%p,内容--%@",mutableCopyStr,mutableCopyStr);

str所指向的地址--0x7fd138d0bd40,内容--123
mutableCopyStr所指向的地址--0x7fd138d0aef0,内容--123

这最后一组代码展示了,当一个可变字符串进行mutableCopy之后同样返回了一个可变的字符串,进行了一次深拷贝,他们分别指向了不同的地址,只是内容还是一样的。

从上面的四组代码可以看出,可变版本与不可变版本之间是可以互相转换的,那么到底是进行了深拷贝还是浅拷贝,不单单只能看到是执行了copy还是mutableCopy,还要结合被拷贝的对象到底是可变的还是不可变的,从而判断出到底是深拷贝还是浅拷贝

  • 不可变版本->copy = 不可变版本(浅拷贝)
  • 不可变版本->mutableCopy = 可变版本(深拷贝)
  • 可变版本->copy = 不可变版本(深拷贝)
  • 可变版本->mutableCopy = 可变版本(深拷贝)

3.数组

数组拷贝操作在日超操作中应该是用的比较多得了,接下来我们来看看关于数组部分的拷贝。

NSArray *array = @[[NSMutableString stringWithString:@"1"],[NSMutableString stringWithString:@"2"]];
NSArray *copyArray = [array copy];
NSMutableArray *mutableCopyArray = [array mutableCopy];
NSLog(@"array所指向的地址%p",array);
NSLog(@"copyArray所指向的地址%p",copyArray);
NSLog(@"mutableCopyArray所指向的地址%p",mutableCopyArray);
NSLog(@"array第一个元素所指向的地址%p,array第二个元素所指向的地址%p",array[0],array[1]);
NSLog(@"copyArray第一个元素所指向的地址%p,copyArray第二个元素所指向的地址%p",copyArray[0],copyArray[1]);
NSLog(@"mutableCopyArray第一个元素所指向的地址%p,mutableCopyArray第二个元素所指向的地址%p",mutableCopyArray[0],mutableCopyArray[1]);

array所指向的地址0x7f92a8ead290
copyArray所指向的地址0x7f92a8ead290
mutableCopyArray所指向的地址0x7f92a8eacf70

array中第一个元素所指向的地址0x7f92a8eaa870,array中第二个元素所指向的地址0x7f92a8e1dca0
copyArray中第一个元素所指向的地址0x7f92a8eaa870,copyArray中第二个元素所指向的地址0x7f92a8e1dca0
mutableCopyArray中第一个元素所指向的地址0x7f92a8eaa870,mutableCopyArray中第二个元素所指向的地址0x7f92a8e1dca0

我们观察上述代码和打印出来的地址,当NSArray进行copy之后返回了一个不可变的NSArray,他们所指向的地址都是同一个,进行了浅拷贝。然后当NSArray进行mutableCopyArray之后返回了一个可变的数组NSMutableArray,他们指向了不同的地址。和上一节中字符串结论是一致的
接下来我们看数组中得元素,我们发现不管是进行了copy还是mutableCopy,这些元素的地址都没有变过,说明数组中得元素都是进行了浅拷贝,进行了指针拷贝而已。为了进一步证实这个说法。我们接下来修改数组中得元素:

    //给第一个元素中追加字符串one,然后分别打印这几个数组
    [array[0] appendString:@"one"];
    NSLog(@"%@",array);
    NSLog(@"%@",copyArray);
    NSLog(@"%@",mutableCopyArray);
    
2015-11-26 15:45:11.632 NSCopying[3379:206715] (
    1one,
    2
)
2015-11-26 15:45:11.632 NSCopying[3379:206715] (
    1one,
    2
)
2015-11-26 15:45:11.632 NSCopying[3379:206715] (
    1one,
    2
)

我们可以看到每一份数组的中第一个元素都发生了变化。这也说明了数组中的元素只是进行了一份浅拷贝。

//接着上面的代码,我们向mutableCopyArray添加一点元素
[mutableCopyArray addObject:[NSMutableString stringWithString:@"3"]];
NSLog(@"array--%@",array);
NSLog(@"copyArray--%@",copyArray);
NSLog(@"mutableCopyArray--%@",mutableCopyArray);

2015-11-26 15:56:38.776 NSCopying[3455:212121] array--(
    1,
    2
)
2015-11-26 15:56:38.776 NSCopying[3455:212121] copyArray--(
    1,
    2
)
2015-11-26 15:56:38.776 NSCopying[3455:212121] mutableCopyArray--(
    1,
    2,
    3
)

根据上一节字符串中结论,mutableCopyArray它是一个深拷贝,所以给它添加元素,并不会影响其他两个数组中得元素。

//我们给mutableCopyArray中第一个元素赋值一个新的字符串,并打印
mutableCopyArray[0] = [NSMutableString stringWithString:@"123"];
NSLog(@"array--%@",array);
NSLog(@"copyArray--%@",copyArray);
NSLog(@"mutableCopyArray--%@",mutableCopyArray);

2015-11-26 16:04:07.464 NSCopying[3486:215020] array--(
    1,
    2
)
2015-11-26 16:04:07.464 NSCopying[3486:215020] copyArray--(
    1,
    2
)
2015-11-26 16:04:07.465 NSCopying[3486:215020] mutableCopyArray--(
    123,
    2,
    3
)

上面的代码创建了一个新的NSMutableString对象并将其内存地址赋给指针mutableCopyArray[0],而其他数组指针指向的对象不变,所以输出时其他数组不受影响。

//接下来我们删除mutableCopyArray中第二个元素,并打印
[mutableCopyArray removeObjectAtIndex:1];
NSLog(@"array--%@",array);
NSLog(@"copyArray--%@",copyArray);
NSLog(@"mutableCopyArray--%@",mutableCopyArray);

2015-11-26 16:10:03.562 NSCopying[3509:217545] array--(
    1,
    2
)
2015-11-26 16:10:03.563 NSCopying[3509:217545] copyArray--(
    1,
    2
)
2015-11-26 16:10:03.563 NSCopying[3509:217545] mutableCopyArray--(
    1
)   

移除的是mutableCopyArray指向的索引结合中的第二个元素,即移除的是对对象的引用,而不是移除数组中的第二个对象。并不会会影响到array,copyArray。

总结:对于数组进行的copy和mutableCopy结论可以参照上一节中字符串结论,数组特殊地方在于其内部的元素,只是进行了简单的浅拷贝,如果数组中得元素它是一个可变对象,比如一个可变字符串,如果你追加了字符串,再或者可以是一个自定义的类,你修改其中某一个属性,其他的数组中得元素也会随之受到相同的修改

4.字典

关于字典拷贝,原理和上一节的数组是一样的,而且其内部的value也是浅拷贝。

NSDictionary *dict = @{@"key1":[NSMutableString stringWithString:@"1"],@"key2":[NSMutableString stringWithString:@"2"]};
NSDictionary *copyDict = [dict copy];
NSMutableDictionary *mutableCopyDict = [dict mutableCopy];

NSLog(@"dict所指向的地址%p",dict);
NSLog(@"copyDict所指向的地址%p",copyDict);
NSLog(@"mutableCopyDict所指向的地址%p",mutableCopyDict);

NSLog(@"%p",dict[@"key1"]);
NSLog(@"%p",copyDict[@"key1"]);
NSLog(@"%p",mutableCopyDict[@"key1"]);

2015-11-26 16:37:31.952 NSCopying[3619:229287] dict所指向的地址0x7fcab0e6c250
2015-11-26 16:37:31.953 NSCopying[3619:229287] copyDict所指向的地址0x7fcab0e6c250
2015-11-26 16:37:31.953 NSCopying[3619:229287] mutableCopyDict所指向的地址0x7fcab0e6c290
2015-11-26 16:37:31.953 NSCopying[3619:229287] 0x7fcab0e689a0
2015-11-26 16:37:31.954 NSCopying[3619:229287] 0x7fcab0e689a0
2015-11-26 16:37:31.954 NSCopying[3619:229287] 0x7fcab0e689a0

字典对象间的拷贝还是遵循第一节中字符串那个结论,根据打印出来的地址可以看出,字典内部的value值和数组中得元素一样都是进行了浅拷贝。

//给dict[@"key1"]追加字符串
[dict[@"key1"] appendString:@"one"];
NSLog(@"dict%@",dict);
NSLog(@"copyDict%@",copyDict);
NSLog(@"mutable%@",mutableCopyDict);

2015-11-26 16:42:19.316 NSCopying[3657:231891] dict{
    key1 = 1one;
    key2 = 2;
}
2015-11-26 16:42:19.821 NSCopying[3657:231891] copyDict{
    key1 = 1one;
    key2 = 2;
}
2015-11-26 16:42:23.133 NSCopying[3657:231891] mutable{
    key1 = 1one;
    key2 = 2;
}

因为value是浅拷贝,value又是可变字符串转,当追加字符串进去的时候,其他字典也会随之受到影响。

//我们给mutableCopyDict中添加一个key-value
[mutableCopyDict setObject:@"3" forKey:@"key3"];
NSLog(@"dict%@",dict);
NSLog(@"copyDict%@",copyDict);
NSLog(@"mutable%@",mutableCopyDict);

2015-11-26 16:46:07.813 NSCopying[3678:234004] dict{
    key1 = 1;
    key2 = 2;
}
2015-11-26 16:46:07.813 NSCopying[3678:234004] copyDict{
    key1 = 1;
    key2 = 2;
}
2015-11-26 16:46:07.813 NSCopying[3678:234004] mutable{
    key1 = 1;
    key2 = 2;
    key3 = 3;
}

跟上一节的数组原理一样,mutableCopyDict本身是一个深拷贝,所以给它内部添加元素,对应删除元素,都不会影响到其他几个字典。提醒:字典中得key你会发现它必须实现NSCopying协议,所以key它本身也进行了拷贝,为什么要这么做,大家自己去思考吧。

小结:可变与不可变对象是可以互相转换的。数组中的元素和字典中得value都是进行了浅拷贝,如果想实现深拷贝,需要自己额外的去实现了。

推荐阅读更多精彩内容

  • 前言 不敢说覆盖OC中所有copy的知识点,但最起码是目前最全的最新的一篇关于 copy的技术文档了。后续发现有新...
    zyydeveloper阅读 1,910评论 4 32
  • 简述深浅拷贝 我们实例化的对象存储在堆区,而指向对象的指针一般存储在栈区。我们需要知道这个前提。  实际上拷贝分为...
    刀客传奇阅读 3,756评论 11 6
  • 框架: 就是系统(苹果)或者第三方(其他的一些高手)事先写好了一些很牛X功能的类.把这些类交给我们使用.这些类的集...
    指尖书法阅读 1,450评论 0 9
  • iOS面试小贴士 ———————————————回答好下面的足够了------------------------...
    不言不爱阅读 1,148评论 0 7
  • 史上最全的iOS面试题及答案 iOS面试小贴士———————————————回答好下面的足够了----------...
    Style_伟阅读 1,784评论 0 35