Hello, World!

对象拷贝

字数统计: 1.8k阅读时长: 7 min
2018/02/08 Share

NSObject类提供了两个用于拷贝的方法:- (id)copy 和- (id)mutableCopy,这两个方法都可以复制已有对象生成副本。

由于Objective-C中几乎所有的类都继承自NSObject,所以类中都有copy和mutableCopy两个方法,那么是否就意味着对象可以直接调用这两个方法进行拷贝了呢?

  • 定义一个AMPerson类继承自NSObject 进行测试,代码如下:
1
2
AMPerson *p1 = [[AMPerson alloc] init];
AMPerson *p2 = [p1 copy];

运行程序,发生崩溃,并输出以下错误信息:
-[AMPerson copyWithZone:]: unrecognized selector sent to instance 0x7bf5e880
错误信息意思是:AMPerson类中找不到copyWithZone:方法。

  • 把copy方法换成mutableCopy,
1
2
AMPerson * p2 = [[AMPerson alloc] init];
AMPerson *p2 = [p1 mutableCopy];

运行之后,依然发生崩溃,并输出以下错误信息:
-[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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface AMPerson: NSObject <NSCopying>

@property (copy,nonatomic)NSString *name;

@end

@implementation AMPerson

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

AMPerson *p = [[[self class] allocWithZone:zone] init];
p.name = [self.name copy];
return p;

}

@end

运行代码

1
2
3
4
AMPerson *p1 = [[AMPerson alloc] init];
AMPerson *p2 = [p1 copy];

NSLog(@"p1 = %p,p2 = %p", p1, p2);

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
2
3
4
5
6
7
8
9
AMPerson *p1 = [[AMPerson alloc] init];
AMPerson *p2 = [[AMPerson alloc] init];
AMPerson *p3 = [[AMPerson alloc] init];

NSArray *ps = @[p1, p2, p3];
NSArray *psCopy = [ps mutableCopy]; // 复制

NSLog(@"p = %p, pCopy = %p", ps, psCopy);
NSLog(@"p = %p, pCopy = %p", ps[0], psCopy[0]);

log信息如下:
p = 0x6000039f3ab0, pCopy = 0x6000039f3840
p = 0x6000035ac9b0, pCopy = 0x6000035ac9b0

结果表明:数组复制只是单单对于数组对象本身而言是深复制,而数组的成员对象默认仍然是浅拷贝的。我们称之为单层深复制。

那么要想实现完全深复制该怎么办呢? 尤其是当该对象包含大量的指针类型的实例变量时,如果某些实例变量里再次包含指针类型的实例变量,那么实现完全深复制会更加复杂。上面的深复制就是因为集合对象中可能会包含指针类型的实例变量,从而导致深复制不完全。

解决方法很简单,复制的代码换成NSArray *pCopy = [[NSMutableArray alloc] initWithArray:p copyItems:YES]即可。

如下所示

1
2
3
4
5
6
7
8
9
AMPerson *p1 = [[AMPerson alloc] init];
AMPerson *p2 = [[AMPerson alloc] init];
AMPerson *p3 = [[AMPerson alloc] init];

NSArray *ps = @[p1, p2, p3];
NSArray *psCopy = [[NSMutableArray alloc] initWithArray:ps copyItems:YES];//复制

NSLog(@"p = %p, pCopy = %p", ps, psCopy);
NSLog(@"p = %p, pCopy = %p", ps[0], psCopy[0]);

log信息如下:
p = 0x600003dae190, pCopy = 0x600003dae3d0
p = 0x6000031e42a0, pCopy = 0x6000031e42f0

结果表明这次的复制是 完全深复制。不仅仅复制了第一层的数组对象,也复制了数组内部的指针类型的实例变量。当然内部的实例变量要遵守NSCoping协议。

copy修饰属性

上面介绍完了copy相关的知识点,那么趁热打铁了解一下为什么字符串的属性,要用copy修饰。

介绍之前,先回忆一下属性修饰符都有哪些:

1
2
3
4
5
6
7
8
9
10
11
12
MRC:

assign:基本数据类型(当出现循环引用时,也可用assign)
retain:除Block和NSString外的其他对象
copy:一般用于NSString和Block

ARC:

strong:默认
weak:多用于ui和解决循环引用
copy:用于NSString和Block
assign:非OC对象

NSString属于OC对象,我们先不使用Copy修饰,在ARC模式下,声明的属性默认是strong修饰,接下来就演示strong修饰NSString的效果。

先定义一个AMPerson类,如下:

1
2
3
@interface AMPerson: NSObject
@property (strong, nonatomic)NSString *name;
@end

在控制器中的viewDidLoad方法中执行如下代码:

1
2
3
4
5
6
AMPerson *p = [[AMPerson alloc] init];
NSMutableString *str = [NSMutableString string];
[str appendString:@"anmav"];
p.name = str;
[str appendString:@"yc"];
NSLog(@"%@",p.name);

打印结果为:anmavyc

结果分析:如果使用strong修饰NSString类型属性,p.name 指向可变字符串对象的地址,当可变字符串内容发生变化时,p.name相对应的值也发生变化。

使用copy修饰后,将可变字符串拷贝一份,重新开辟内存空间,外部修改mutableString的值,不会对p.name造成影响。

刚刚使用了NSMutableString(可变字符串),对mutableString执行copy操作,属于深拷贝,所以开辟了新的内存空间,如果使用的是NSString(不可变内存),对NSString进行copy属于浅拷贝,不会开辟新的内存空间,是不是就不会出现这个问题了呢?

1
2
3
4
5
6
AMPerson *p = [[AMPerson alloc] init];
NSString *str = [NSString string];
str = @"anmav";
p.name = str;
str = @"yc";
NSLog(@"%@",p.name);

打印结果为:anmav

将之前的可变字符串变为不可变字符串,因为NSString不支持append添加操作,我这里的两次str赋值操作,其实是让str重新指向了一片内存空间,并不是修改了str原本内存中的值。

OC中对象即指针,实际上存储的是内存地址,p.name = str; 实际是将str存储的@”xiaoming”这块地址给了p.name,p.name还指向着@”xiaoming”)。

所以改变str的指向后,p.name的指向并没有改变,输出没有受到影响。

总结

对于对象的深复制的概念没有必要那么纠结,只要我们理解了复制的本质,并且运用到我们的业务场景,选择我们想要的复制方式就可以。最主要的还是理解本质并且学会使用。

CATALOG
  1. 1. 自定义对象
  2. 2. 系统对象
  3. 3. 多层数组
    1. 3.1. copy修饰属性
  4. 4. 总结