Objective-C
语言里的拷贝就是对象拷贝,即创建一个跟之前的实例对象一模一样的对象。一般来说数据模型类是需要支持拷贝的。Objective-C
里拷贝又分为浅拷贝(shallow-copy
)和深拷贝(deep-copy
)。
# 浅拷贝和深拷贝区别
浅拷贝只是复制一下对象指针,但是拷贝前和拷贝后的对象指针均指向同一内存区域,只不过是对象的内存引用计数加一。 深拷贝为拷贝前和拷贝后的对象指针指向不同的内存区域,即会创建一个新的对象。 下图比较直观清楚。
一个非常容易产生的误区就是,遵从了 NSCopying
协议的对象都会执行深拷贝。其实不然,Foundation
框架里大部分类执行的还是浅拷贝。比如 NSString
等。举个例子:
NSString *str = [NSString stringWithFormat:@"%@",@"hello"];
NSString *copyStr = [str copy];
NSLog(@"str = %p,copyStr = %p",str ,copyStr);
---
> str = 0x6f6c6c656855,copyStr = 0x6f6c6c656855
> str 在 copy 前后指向的内存地址都一模一样。
还有一点注意的是,虽然有浅拷贝和深拷贝的概念,但是并没有专门定义深拷贝的协议。这点很关键,这意味着除非有文档是写用深拷贝实现 NSCopying
协议的,否则深拷贝的实现都是需要开发者自己去手写实现,而不能依赖于系统框架的实现。
# Copy 实例方法和 NSCopying 协议
NSObject
有名为 copy
和 mutableCopy
的实例方法。
- (id)copy;
- (id)mutableCopy;
NSCopying
协议的方法为 -(id)copyWithZone:(nullable NSZone *)zone
。(zone
目前没有任何意义)
@protocol NSCopying
- (id)copyWithZone:(nullable NSZone *)zone;
@end
这个实例方法和协议方法有啥关联呢?其实调用 NSObject
的 copy
实例方法就是调用 NSCopying
协议约定的 copyWithZone
方法。同理的,调用 NSObject
的 mutableCopy
实例方法就是调用 NSMutableCopying
协议的 mutableCopyWithZone
方法。
需要注意的是,因为 NSObject
类并不支持 NSCopying
协议,所以继承 NSObject
的子类如果不实现 NSCopying
协议的 copyWithZone
方法会导致应用异常。 见下面 demo
@interface FRModel : NSObject
@end
@implementation FRModel
@end
---
FRModel *model = [[FRModel alloc] init];
[model copy];
> -[FRModel copyWithZone:]: unrecognized selector sent to instance 0x1004004c0
copy
API 说明 (opens new window) Returns the object returned by copyWithZone:.
因为 NSObject
并没有实现 NSCopying
协议,即没有实现 copyWithZone:
方法,所以 FRModel
需要单独实现 copyWithZone:
方法。有一个误区是子类直接复写 copy
方法,这是不正确的操作方式,要避免。
# Copy 和 MutableCopy
上面说了 NSObject
有 copy
和 mutableCopy
两个实例方法。对应的也有两个协议, NSCopying(copyWithZone:)
和 NSMutableCopying(mutableCopyWithZone:)
。虽然名字比较像但是这是两个不同的类,即如果你的类只实现了 NSCopying
协议,这时候给你发 mutableCopy
方法的话还是照样会异常,这两个也没有任何包含关系,把这两个类理解为单独的协议就好了。
FRModel *model = [[FRModel alloc] init];
model.name = @"Frank";
FRModel *copyModel = [model mutableCopy];
NSLog(@"model = %p,copymodel = %p",model,copyModel);
NSLog(@"model.name = %p,copymodel.name = %p",model.name,copyModel.name);
---
> FRCopyDemo *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[FRModel mutableCopyWithZone:]: unrecognized selector sent to instance 0x100532040'
# 什么时候用 Copy 什么时候用 MutableCopy?
我们可以看下 NSMutableCopying
的协议说明 (opens new window)
The
NSMutableCopying
protocol declares a method for providing mutable copies of an object. Only classes that define an “immutable vs. mutable” distinction should adopt this protocol. Classes that don’t define such a distinction should adoptNSCopying
instead.NSMutableCopying
声明了提供可对象可变副本的方法,只有区分可变和不可变的类应该遵守这个协议,不区分可变不尅版的话就直接用 NSCopying 方法就好了。
举个例子,我们日常使用的包括可变类型的类是 NSString(NSMutableString)
,NSArray(NSMutableArray)
,NSDictionary(NSMutableDictionary)
以及NSSet(NSMutableSet)
这些类都是同时遵守了 NSCopying
和 NSMutableCopying
协议的。一般来说,我们自己用到的类很少有是可变类型的,所以也很少看到有自己的类实现 NSMutableCopying
协议。
如果要实现的话基本遵守如下原则:
- 向可变(
mutable
)或者不可变(immutable
)对象发送copy
消息,得到的都是不可变(immutable
)对象。 - 向可变(
mutable
)或者不可变(immutable
)对象发送mutableCopy
消息,得到的都是可变(mutable
)对象。 举个例子验证一下
NSString *str = [NSString stringWithFormat:@"%@",@"hello"];
NSMutableString *copyStr = [str mutableCopy]; //得到可变对象.
[copyStr appendString:@" world"];
NSLog(@"str = %p,copyStr = %p",str ,copyStr);
NSLog(@"str class = %@,copyStr class = %@",NSStringFromClass([str class]) ,NSStringFromClass([copyStr class]));
NSLog(@"str = %@,copyStr = %@",str ,copyStr);
---
> FRCopyDemo str = 0x6f6c6c656855, copyStr = 0x10044a960
> FRCopyDemo str class = NSTaggedPointerString, copyStr class = __NSCFString
> FRCopyDemo str = hello, copyStr = hello world
通过输出结果基本上验证了以上的结论。
这儿还有一点比较有意思的事儿是,关于 mutableCopy
和深拷贝的关系。我们看到 mutableCopy
之后对象的指针发生了变化,内容并没有发生变化。但其实对象指针类型已经发生了变化。所以跟我们之前说的拷贝多少还是有区别的(正常的拷贝指针类型是不会发生变化的)。只要搞清楚这些不同的概念,就能理解这些概念的区别所在。
# 怎样让我们的对象支持 copy
完整的回答应该是:让类实现 NSCopying
协议里的 copyWithZone
: 方法,如果对象是可变的应该同时实现 NSMutableCopying
的 mutableCopyWithZone:
方法。
# copyWithZone: 方法的最佳实践
一般开发者自己实现 copyWithZone:
方法的时候通常都是实现深拷贝,而非浅拷贝。因为浅拷贝确实没啥好实现的。深拷贝的实现需要考虑其父类是否也遵守了 NSCopying
协议,实现了 copyWithZone:
方法。
比如继承自 NSObject
的 FRModel
类按如下方式实现 copyWithZone:
方法,这样显然是会异常的(实际上连编译都不会编译过去)。
@interface FRModel : NSObject<NSCopying>
@end
@implementation FRModel
- (id)copyWithZone:(NSZone *)zone {
FRModel *frmodel = [[super allocWithZone:zone] init];
return frmodel;
}
@end
稍作修改,如果父类没有实现的话子类直接重新创建一个就好了
@interface FRModel : NSObject<NSCopying>
@end
@implementation FRModel
- (id)copyWithZone:(NSZone *)zone {
FRModel *frmodel = [[[self class] allocWithZone:zone] init];
return frmodel;
}
@end
如果该类还有属性或者成员变量的话,可以直接对成员变量执行 copy
操作。参考如下 demo:
@interface FRModel : NSObject<NSCopying>
@property (nonatomic,strong) NSString *name;
@end
@implementation FRModel
- (id)copyWithZone:(NSZone *)zone {
FRModel *frmodel = [[FRModel allocWithZone:zone] init];
if (frmodel) { frmodel.name = [_name copyWithZone:zone]; }
return frmodel;
}
@end
FRModel *model = [[FRModel alloc] init];
model.name = @"Frank";
FRModel *copyModel = [model copy];
NSLog(@"model = %p,copymodel = %p",model,copyModel);
NSLog(@"model.name = %p,copymodel.name = %p",model.name,copyModel.name);
---
> FRCopyDemo model = 0x102803520,copymodel = 0x102803530
> FRCopyDemo model.name = 0x1000010d0,copymodel.name = 0x1000010d0
注意因为 NSString
类型本身 copyWithZone:
属性为浅拷贝,所以最后输出结果两个类的 name
属性的指针指向相同。
Best practice when implementing copywithzone (opens new window)
# 集合类型拷贝
Foundation
框架中所有集合类型在默认情况下都执行浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中数据。这样做的原因在于,容器内的对象未必能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中每一个对象。
集合类型的浅拷贝和深拷贝的概念和普通对象的深浅拷贝略有不同。
- 集合类型的浅拷贝是指,当执行浅拷贝的时候,原始集合类型里的对象都收到一个
retain
消息,对象指针被复制到新的集合类型里。 - 集合类型的深拷贝是指,当执行深拷贝的时候,原始集合里的对象都会收到一个
copyWithZone:
,即集合里的对象需要实现NSCopying
协议来实现深拷贝。如果集合里的对象并没有实现NSCopying
则程序会异常。 如图所示 浅拷贝 demo:
NSObject *obj = [[NSObject alloc] init];
NSArray *arr = [NSArray arrayWithObject:obj];
NSArray *copyArr = [[NSArray alloc] initWithArray:arr copyItems:NO];
NSLog(@"arr = %p,copyArr = %p",arr ,copyArr);
NSLog(@"arr model = %p,copyArr model = %p",[arr objectAtIndex:0],[copyArr objectAtIndex:0]);
---
> FRCopyDemo arr = 0x100462b40,copyArr = 0x100461400
> FRCopyDemo arr model = 0x100400640,copyArr model = 0x100400640
里面使用了 initWithArray:copyItems:
这个方法,传入 NO 即为浅拷贝。通过打印日志可以看到集合对象指针虽然发生了变化,但是集合内部元素的对象指针还是指向同样的内存区域。
深拷贝 demo: (将 initWithArray:copyItems:
方法第二个参数传入 YES 即为深拷贝)
NSObject *obj = [[NSObject alloc] init];
NSArray *arr = [NSArray arrayWithObject:obj];
NSArray *copyArr = [[NSArray alloc] initWithArray:arr copyItems:YES];
NSLog(@"arr = %p,copyArr = %p",arr ,copyArr);
NSLog(@"arr model = %p,copyArr model = %p",[arr objectAtIndex:0],[copyArr objectAtIndex:0]);
---
> FRCopyDemo -[NSObject copyWithZone:]: unrecognized selector sent to instance 0x100688490
> FRCopyDemo *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSObject copyWithZone:]: unrecognized selector sent to instance 0x100688490'
因为 NSObject
没有实现 NSCopying
协议,所以集合类型深拷贝时,集合元素 NSObject
对象收到 copyWithZone:
之后异常了。我们将 NSObject 类型修改为 NSMutableString
再次运行
NSMutableString *obj = [NSMutableString stringWithFormat:@"hello world"];
NSArray *arr = [NSArray arrayWithObject:obj];
NSArray *copyArr = [[NSArray alloc] initWithArray:arr copyItems:YES];
NSLog(@"arr = %p,copyArr = %p",arr ,copyArr);
NSLog(@"arr model = %p,copyArr model = %p",[arr objectAtIndex:0],[copyArr objectAtIndex:0]);
---
> FRCopyDemo arr = 0x100406dc0,copyArr = 0x100403fb0
> FRCopyDemo arr model = 0x100406780,copyArr model = 0x100405260
可以看到集合内的元素执行了不同的内存地址,因为 NSMutableString
收到 copyWithZone:
消息会生成一个不可变的 NSString
对象。
initWithArray:copyItems:
API 说明 (opens new window)
copyItems:
参数flag
IfYES
, each object in array receives acopyWithZone:
message to create a copy of the object—objects must conform to theNSCopying
protocol. In a managed memory environment, this is instead of the retain message the object would otherwise receive. The object copy is then added to the returned array. IfNO
, then in a managed memory environment each object in array simply receives aretain
message when it is added to the returned array.
# 单层复制和完全复制?
思考这样一种情况,即数组套数组,这种深拷贝是怎么做?
我们刚使用 initWithArray:copyItems:
方法进行的拷贝只是元素拷贝。即如果是两层数组的话,内层数组里的元素其实并没有机会执行 copyWithZone:
方法。苹果官方称这种拷贝为 one-level-deep copy
,即单层复制。
完全复制是指,不管嵌套多少层集合,每层的的元素都有机会执行 copyWithZone:
方法。
# Copy 和 NSCoding
如何实现完全复制呢?
让对象实现 NSCoding
协议,然后将对象归档到文件里再从文件中归档出来,即需要进行两次 I/O 操作。
举个例子:
//NSArray 和 NSString 都支持 NSCoding 协议
NSString *path = @"/Users/xiushan.fan/Desktop/arrfile";
NSArray *subArray1 = @[[NSMutableString stringWithString:@"1"]];
NSArray *subArray2 = @[[NSMutableString stringWithString:@"2"]];
NSArray *wholeArr = @[subArray1,subArray2];
[NSKeyedArchiver archiveRootObject:wholeArr toFile:path];
NSLog(@"wholeArr = %@,wholeArr = %p,subArr1 = %p,elemement = %p",wholeArr,wholeArr,[wholeArr objectAtIndex:0],[[wholeArr objectAtIndex:0] objectAtIndex:0]);
NSArray *unarchivedArray = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
//NSArray *unarchivedArray = [[NSArray alloc] initWithArray:wholeArr copyItems:YES];
NSLog(@"unarchivedArray = %@, unarchivedArray = %p ,subArr1 = %p,elemement = %p",unarchivedArray,unarchivedArray,[unarchivedArray objectAtIndex:0],[[unarchivedArray objectAtIndex:0] objectAtIndex:0]);
---
> FRCopyDemo wholeArr = ( ( 1 ),( 2 ) ),wholeArr = 0x100422c60,subArr1 = 0x100406930,elemement = 0x100422580
> FRCopyDemo unarchivedArray = (
(1 ), ( 2 )), unarchivedArray = 0x1004273a0 ,subArr1 = 0x1004039c0,elemement = 0x100424230
可以看到 NSCoding
进行转化之后所有的元素的内存地址均不相同。
# Copy 和属性
被 copy attribute
修饰的属性,在被赋值的时候,新值其实是会收到一个 copyWithZone:
的消息
@interface FRObj : NSObject<NSCopying>
@end
@implementation FRObj
- (id)copyWithZone:(NSZone *)zone {
FRObj *obj = [[FRObj allocWithZone:zone] init];
NSLog(@"FRObj copy with zone self %@",self);
return obj;
}
@end
@interface FRModel : NSObject<NSCopying>
@property (nonatomic,copy) FRObj *name;
@end
FRModel *model = [[FRModel alloc] init];
FRObj *obj = [[FRObj alloc] init];
NSLog(@"obj %@",obj);
model.name = obj;
---
> FRCopyDemo obj <FRObj: 0x1004001c0>
> FRCopyDemo FRObj copy with zone self <FRObj: 0x1004001c0>
可以看到 FRObj
赋值的时候自己收到了一条 copyWithZone:
的消息。
具体的底层实现可以参考 属性 attribute 总结 (opens new window) 里的 copy attribute
部分
# 一些 QA
Q: copy 关键字一般在哪些场景下使用?
A: 1.NSString/NSArray/NSDictionary
这些类使用,因为这些类都有对应的可变类型。 2. MRC 下修饰 block
属性需要使用 copy
。ARC 下可以使用 copy/strong
去修饰 block
,一般也使用 copy
,给人感觉比较直观。
这个的对应的问题是,
NSString
使用什么修饰符修饰?为什么?答案是使用copy
修饰符,原因是NSString
用strong
修饰符的时候如果指向自己的可变类型,当可变类型内容修改的时候,NSString
属性也会跟着一起修改,这不是我们希望看到的。同时根据刚才的分析,使用copy
修饰并不会给NSString
造成额外的负担,因为仅仅是浅拷贝而已。
Q: 这个写法会出什么问题: @property (copy) NSMutableArray *array;
A: 当给 array
赋值的时候可变对象会变为不可变对象,其实是向被赋值的对象发送了一个 copy
消息, copy
的默认实现就是将可变对象变为不可变对象。属性里也没有 mutablecopy
这种修饰符,所以只能手动发送 mutableCopy
消息达到目的。
Property of mutable type 'NSMutableDictionary' has 'copy' attribute; an immutable object will be stored instead (opens new window)
Q: 如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?
A: 实现 NSCopying
协议,实现 copyWithZone:
方法. 直接发送 copy
消息就好了
# 参考地址
Collections Programming Topics - Copying Collections (opens new window) Cocoa Core Competencies - Object copying (opens new window) Effective Objective-C 2.0 - 理解 NSCopying 协议 Advanced Memory Management Programming Guide - About Memory Management (opens new window) iOS 集合的深复制与浅复制 (opens new window) Objective-C copy,看我就够了 (opens new window) Objective-C copy 那些事儿 (opens new window)