NSObject类提供了两个用于拷贝的方法:- (id)copy 和- (id)mutableCopy,这两个方法都可以复制已有对象生成副本。
由于Objective-C中几乎所有的类都继承自NSObject,所以类中都有copy和mutableCopy两个方法,那么是否就意味着对象可以直接调用这两个方法进行拷贝了呢?
- 定义一个AMPerson类继承自NSObject 进行测试,代码如下:
1 | AMPerson *p1 = [[AMPerson alloc] init]; |
运行程序,发生崩溃,并输出以下错误信息:
-[AMPerson copyWithZone:]: unrecognized selector sent to instance 0x7bf5e880
错误信息意思是:AMPerson类中找不到copyWithZone:方法。
- 把copy方法换成mutableCopy,
1 | AMPerson * p2 = [[AMPerson alloc] init]; |
运行之后,依然发生崩溃,并输出以下错误信息:
-[AMPerson mutableCopyWithZone:]: unrecognized selector sent to instance 0x7a2415f0
错误信息意思是:AMPerson类中找不到mutableCopyWithZone:方法。
由以上错误可知:拷贝操作表面是调用copy和mutableCopy方法,其实底层是调用对象自身的copyWithZone和mutableCopyWithZone方法来完成实际的复制工作。
copy返回实际上就是copyWithZone:方法的返回值,mutableCopy与mutableCopyWithZone:方法也是同样的道理。
由该例就引出了下面的讨论内容了。就是对象具体要满足什么条件,才可以被复制。
自定义对象
要想自定义对象可以复制,那么该类就必须
一,遵守NSCopying 或 NSMutableCopying协议。
二,实现协议中copyWithZone或者mutableCopyWithZone方法。
所以为了让AMPerson类能够复制自身,我们需要让AMPerson遵守NSCopying协议,实现copyWithZone:方法。
1 | @interface AMPerson: NSObject <NSCopying> |
运行代码
1 | AMPerson *p1 = [[AMPerson alloc] init]; |
log信息如下:
p1 = 0x7969cc40,p2 = 0x7969c6e0
结果表明:p1和p2是两个地址不同的对象,复制操作成功。
系统对象
copy方法用于复制对象的副本,通常来说,copy方法总是返回对象的不可修改副本,即使对象本身是可修改的。例如,NSMutableString调用copy方法,将会返回不可修改的字符串对象。
mutableCopy方法用于复制对象的可变副本,通常来说,mutableCopy方法总是返回对象可修改的副本,即使被复制的对象本身是不可修改的。例如,程序调用NSString的mutableCopy方法,将会返回一个NSMutableString对象。
下图详细列出了常用的NSString、NSMutableString、NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等调用copy与mutableCopy方法后的结果。
深复制与浅复制
对象拷贝有两种方式:浅拷贝和深拷贝。浅拷贝,并不拷贝对象本身,仅仅是拷贝指向对象的指针;深拷贝是直接拷贝整个对象内容到另一块内存中。
再简单些说:浅拷贝就是指针拷贝,深拷贝就是内容拷贝。
多层数组
在多层数组中,对第一层进行内容拷贝,其它层进行指针拷贝,这种情况是属于深复制,还是浅复制?
如下所示
1 | AMPerson *p1 = [[AMPerson alloc] init]; |
log信息如下:
p = 0x6000039f3ab0, pCopy = 0x6000039f3840
p = 0x6000035ac9b0, pCopy = 0x6000035ac9b0
结果表明:数组复制只是单单对于数组对象本身而言是深复制,而数组的成员对象默认仍然是浅拷贝的。我们称之为单层深复制。
那么要想实现完全深复制该怎么办呢? 尤其是当该对象包含大量的指针类型的实例变量时,如果某些实例变量里再次包含指针类型的实例变量,那么实现完全深复制会更加复杂。上面的深复制就是因为集合对象中可能会包含指针类型的实例变量,从而导致深复制不完全。
解决方法很简单,复制的代码换成NSArray *pCopy = [[NSMutableArray alloc] initWithArray:p copyItems:YES]即可。
如下所示
1 | AMPerson *p1 = [[AMPerson alloc] init]; |
log信息如下:
p = 0x600003dae190, pCopy = 0x600003dae3d0
p = 0x6000031e42a0, pCopy = 0x6000031e42f0
结果表明这次的复制是 完全深复制。不仅仅复制了第一层的数组对象,也复制了数组内部的指针类型的实例变量。当然内部的实例变量要遵守NSCoping协议。
copy修饰属性
上面介绍完了copy相关的知识点,那么趁热打铁了解一下为什么字符串的属性,要用copy修饰。
介绍之前,先回忆一下属性修饰符都有哪些:
1 | MRC: |
NSString属于OC对象,我们先不使用Copy修饰,在ARC模式下,声明的属性默认是strong修饰,接下来就演示strong修饰NSString的效果。
先定义一个AMPerson类,如下:
1 | @interface AMPerson: NSObject |
在控制器中的viewDidLoad方法中执行如下代码:
1 | AMPerson *p = [[AMPerson alloc] init]; |
打印结果为:anmavyc
结果分析:如果使用strong修饰NSString类型属性,p.name 指向可变字符串对象的地址,当可变字符串内容发生变化时,p.name相对应的值也发生变化。
使用copy修饰后,将可变字符串拷贝一份,重新开辟内存空间,外部修改mutableString的值,不会对p.name造成影响。
刚刚使用了NSMutableString(可变字符串),对mutableString执行copy操作,属于深拷贝,所以开辟了新的内存空间,如果使用的是NSString(不可变内存),对NSString进行copy属于浅拷贝,不会开辟新的内存空间,是不是就不会出现这个问题了呢?
1 | AMPerson *p = [[AMPerson alloc] init]; |
打印结果为:anmav
将之前的可变字符串变为不可变字符串,因为NSString不支持append添加操作,我这里的两次str赋值操作,其实是让str重新指向了一片内存空间,并不是修改了str原本内存中的值。
OC中对象即指针,实际上存储的是内存地址,p.name = str; 实际是将str存储的@”xiaoming”这块地址给了p.name,p.name还指向着@”xiaoming”)。
所以改变str的指向后,p.name的指向并没有改变,输出没有受到影响。
总结
对于对象的深复制的概念没有必要那么纠结,只要我们理解了复制的本质,并且运用到我们的业务场景,选择我们想要的复制方式就可以。最主要的还是理解本质并且学会使用。