什么是copy?什么是iOS的copy?
copy主要用于copy一份新的数据并与原数据相互独立存在。
对于基本数据类型(非对象)而言,一般会叫赋值操作,即将非对象的值赋予另外一个非对象。
对于对象而言,iOS中是用指针来操作的。因此,对于其赋值操作相当于copy了指针,使得源指针和新指针都指向同一个对象。对于copy操作,分为浅拷贝和深拷贝两种类型,下面会详述。
在 iOS 中并不是所有的对象都支持 copy/mutableCopy,遵守 NSCopying 协议的类可以发送 copy 消息,遵守 NSMutableCopying 协议的类可以发送 mutableCopy 消息。假如对一个没有遵守上述两协议的对象发送了 copy 或者 mutableCopy,那么就会发生异常。因此对于自定义对象,需要做相应的处理,下面会详述。
iOS中属性的copy修饰符
举例如下
@property (nonatomic, copy) NSString *s;
其setter方法默认为:
- (void)setS:(NSString *)s {
_s = [s copy];
}
要想设置该对象的特性为copy,该对象必须遵守NSCopying协议,Foundation类默认实现了NSCopying协议,所以只需要为自定义的类实现该协议即可。此外即使定义实例变量时使用了可变类型,但是要使用了copy修饰符,实例变量实际得到的值也是不可变的。
不可变拷贝(copy)还是可变拷贝(mutableCopy)?
对象分不可变对象和可变对象,复制方法有 copy 和 mutableCopy。
copy 返回的是不可变对象(immutableObject),mutableCopy 返回的是可变对象(mutableObject)
浅拷贝(Shallow Copy)还是深拷贝(Deep Copy)?
无论浅拷贝还是深拷贝,被拷贝的对象都会被复制一份,有新的对象产生,而浅拷贝只是复制指针,源对象的引用计数器 +1,其实相当于做了一次 retain 操作。而深拷贝会复制指针指向的内容,源对象引用计数器不变,赋值对象的引用计数器设置为 1。举例来描述一下:
对普通对象 ObjectA 进行 copy,无论浅拷贝还是深拷贝,都会复制出一个新的对象 ObjectB,只是浅拷贝时 ObjectA 与 ObjectB 中的指针还指向同一个对象,而深拷贝时 ObjectA 和 ObjectB 中的 指针分别指向各自的对象(对象内容被复制了一份)。
一句话总结:浅拷贝是指针拷贝,深拷贝是内容拷贝。
浅拷贝和深拷贝的区别来源于copyWithZone:方法的实现。浅拷贝拷贝出来的对象与源对象地址一致,修改拷贝对象的值会直接影响源对象的值。深拷贝出来的对象与源对象地址不一致,修改拷贝对象的值对源对象的值没有任何影响。深拷贝是直接拷贝整个对象内容到另一块内存中。
原生对象的拷贝
先抛结论:
只有不可变对象创建不可变副本(copy)才是浅拷贝,其它都是深拷贝。
大家带着结论阅读下面的详细阐述:
系统的非容器类(非集合类)对象的copy与mutableCopy
系统的非容器类对象指的是 NSString,NSNumber 这些不能包含其他对象的对象。在非集合类对象中,对不可变对象进行copy操作,是浅拷贝,mutableCopy操作是深拷贝。对可变对象进行copy和mutableCopy都是深拷贝,但是 copy 返回的对象是不可变的。
(Foundation对于不可变复制对象而言,copy方法做了优化,不会开辟新的内存,是浅拷贝,相当于retain)
用代码简单表示如下:
NSString *str = @"hello world!";
NSString *strCopy = [str copy] // 浅拷贝,strCopy与str的地址一样
NSMutableString *strMCopy = [str mutableCopy] // 深拷贝,strMCopy与str的地址不一样
NSMutableString *mutableStr = [NSMutableString stringWithString: @"hello world!"];
NSString *strCopy = [mutableStr copy] // 深拷贝,但strCopy不可变
NSMutableString *strMCopy = [mutableStr mutableCopy] // 深拷贝
系统的容器类(集合类)对象
系统的容器类对象指 NSArray,NSDictionary 等可以容纳其他对象的对象。对于容器类本身,上面讨论的结论也是适用的,需要探讨的是复制后容器内对象的变化。集合对象的深拷贝仅限于对象本身,对集合内的对象元素仍然是浅拷贝,即单层内容复制。默认只拷贝容器对象本身,不复制其中的数据。这样做的目的是,容器内的对象未必都能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中的某个对象)。
举例描述为:
NSArray *arr = @[@[@"a", @"b"], @[@"c", @"d"];
NSArray *copyArr = [arr copy]; // 浅拷贝
NSMutableArray *mCopyArr = [arr mutableCopy]; //深拷贝,但单层内容复制
NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *copyArr = [mutableArr copy]; // 深拷贝,单层内容复制
NSMutableArray *mCopyArr = [mutableArr mutableCopy]; // 深拷贝,单层内容复制
一般来说,完全深复制的实现复杂度会大一些,尤其是当该对象包含大量的指针类型的实例变量时,如果某些实例变量里再次包含指针类型的实例变量,那么实现完全深复制会更加复杂。那如果确实需要进行深拷贝,那有什么方法吗?当然是有的:
- 集合的单层深复制 (one-level-deep copy)
可以用 initWithArray:copyItems: 将第二个参数设置为YES即可实现单层深复制,如
NSMutableArray *copyArray = [[NSMutableArray alloc] initWithArray:array copyItems:YES];
如果你用这种方法深复制,集合里的每个对象都会收到 copyWithZone: 消息。同时集合里的对象需遵循 NSCopying 协议,否则会崩溃。
- A true deep copy
使用归档来实现:
// 使用归档/反归档拷贝
NSMutableArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject: array]];
结合我们常用的stringWithString:和arrayWithArray:方法再来与拷贝对照:
NSString *t1 = @"hello world";
NSString *t2 = [NSString stringWithString:t1];
NSLog(@"[test]%p,%p",t1,t2);//[test]0x10a403e40,0x10a403e40,浅拷贝
NSMutableString *t3 = [NSMutableString stringWithString:t2];
NSLog(@"[test]%p,%p",t2,t3);//[test]0x10332fe40,0x6000029f6280,深拷贝
NSArray *a1 = @[t1,t2,t3];
NSArray *a2 = [NSArray arrayWithArray:a1];//[test]0x600001b9ed60,0x600001b9edf0,深拷贝
NSLog(@"[test]%p,%p",a1,a2);
NSArray *a3 = [a1 copy];//[test]0x600001c2e280,0x600001c2e280,浅拷贝
NSLog(@"[test]%p,%p",a1,a3);
NSMutableArray *a4 = [NSMutableArray arrayWithArray:a1];//[test]0x600002122fd0,0x6000021564c0,对象本身深拷贝
NSLog(@"[test]%p,%p",a1,a4);
NSLog(@"[test]%p,%p",a1.firstObject,a4.firstObject);//[test]0x10d38fe40,0x10d38fe40,里层对象浅拷贝
NSMutableString *t4 = [NSMutableString stringWithString:t1];
[a4 replaceObjectAtIndex:0 withObject:t4];
NSMutableArray *a5 = [[NSMutableArray alloc] initWithArray:a4 copyItems:YES];//[test]0x600003b34a80,0x600003b473c0,深拷贝
NSLog(@"[test]%p,%p",a4,a5);
NSLog(@"[test]%p,%p",a4.firstObject,a5.firstObject);//[test]0x600003b472d0,0x6000035695e0,单层深拷贝
自定义对象的拷贝
iOS中自定义对象的类并没有默认遵守copy的协议。如果想自定义对象支持copy,就必须遵守 NSCopying,并且实现 copyWithZone: 方法,同理mutableCopy 需遵守 NSMutableCopying,并且实现 mutableCopyWithZone: 方法。
在实现 copyWithZone: 方法时需要注意:copyWithZone: 相当于新创建一个对象,并将当前对象的值复制到新创建的对象中。
- 直接从 NSObject 继承的类,应使用 [[[self class] allocWithZone:zone] init],使得在创建新对象时能够使用正确的类。
-
父类中已经实现了 copyWithZone: 时,应先调用父类的方法,让父类创建对应的对象(self class 能保证创建对象是正确的),并拷贝父类中定义的成员变量。
与copy相关的面试可能遇到的问题
讲一讲iOS中的copy和retain
copy创建的新对象也是强引用,在iOS中分为浅拷贝和深拷贝。浅拷贝是指针复制,与源对象指向同一内存地址,源对象的引用计数器会+1。深拷贝是内容拷贝,两个对象内容相同,新的对象 retain 为 1 ,与旧有对象的引用计数无关,旧有对象没有变化。copy 会减少对象对上下文的依赖。
retain 属性表示两个对象地址相同,内容也相同,这个对象的引用计数器+1。为什么NSString类型的成员变量的修饰属性用copy而不是strong呢?
因为有时候赋给该成员变量的值是NSMutableString类型的,这时候如果修饰符是strong,那成员变量的值就会随着被赋值对象的值的变化而变化。若是用copy修饰,则对NSMutableString类型的值进行了一次深拷贝,成员变量的值就不会随着被赋值对象的值的改变而改变。对NSMutableString用copy修饰会有什么问题?
答:对NSMutableString用copy修饰得到的是不可变类型NSString对象,如果再对这个对象进行改变会crash。
NSString与NSMutableString的区别主要是:NSMutableString对象所指向内存地址中的内容可以被修改,而NSString对象所指向内存地址中内容不能被修改,但NSString对象不是常量,可以通过为NSString对象重新分配一块内存来改变其指向的内容。