NSString Property:copy or retain
iOS程序中,在定义对象属性的时候,我们一般会把NSString
类型的属性的修饰符写成 copy
, 而不是 retain
(或者 ARC 下面的 strong
)。
为什么会有NSString
要用copy
来修饰的convention?能否用 strong
代替?
回答这个问题之前,我们先看段代码:
@interface Fruit : NSObject
@property (nonatomic, copy) NSString * fruitNameCopy;
@property (nonatomic, strong) NSString * fruitNameStrong;
@end
NSMutableString * fruitName = [NSMutableString stringWithString:@"apple"];
Fruit * fruit = [[Fruit alloc] init];
fruit.fruitNameCopy = fruitName;
fruit.fruitNameStrong = fruitName;
NSLog(@"fruitName: %p %@", fruitName, fruitName); // 0x7fddabd0e250 apple
NSLog(@"fruitNameCopy: %p %@", fruit.fruitNameCopy, fruit.fruitNameCopy); // 0xa0000656c7070615 apple
NSLog(@"fruitNameStrong:%p %@", fruit.fruitNameStrong, fruit.fruitNameStrong); // 0x7fddabd0e250 apple
//fruitName =@"pear";
[fruitName setString: @"pear"];
NSLog(@"fruitName: %p %@", fruitName, fruitName); // 0x7fddabd0e250 pear
NSLog(@"fruitNameCopy: %p %@", fruit.fruitNameCopy, fruit.fruitNameCopy); // 0xa0000656c7070615 apple
NSLog(@"fruitNameStrong:%p %@", fruit.fruitNameStrong, fruit.fruitNameStrong); // 0x7fddabd0e250 pear
该段程序运行到结尾时, fruitNameCopy 的值是 apple,fruitNameCopy 的值是 pear,且两者的内存地址也不一样。
程序中,当我们对一个mutable对象做copy
操作的时候,objc runtime 会用一次深拷贝来处理, runtime 会重新分配一块内存地址空间,并把原mutable对象的值拷贝过来。所以打印出来的 fruitNameCopy 和 fruitNameStrong 的内存地址是不一样的。而这之后在对 fruitName 做任何的赋值操作都只能作用于 fruitNameStrong.
那如果我们把 fruitName 改成 immutable 的会是怎样的了?
NSString * fruitName = @"apple";
Fruit * fruit = [[Fruit alloc] init];
fruit.fruitNameCopy = fruitName;
fruit.fruitNameStrong = fruitName;
NSLog(@"fruitName: %p %@", fruitName, fruitName);// 0x10e27b078 apple
NSLog(@"fruitNameCopy: %p %@", fruit.fruitNameCopy, fruit.fruitNameCopy);// 0x10e27b078 apple
NSLog(@"fruitNameStrong:%p %@", fruit.fruitNameStrong, fruit.fruitNameStrong);// 0x10e27b078 apple
fruitName =@"pear";
NSLog(@"fruitName: %p %@", fruitName, fruitName);// 0x10e27b0f8 pear
NSLog(@"fruitNameCopy: %p %@", fruit.fruitNameCopy, fruit.fruitNameCopy);// 0x10e27b078 apple
NSLog(@"fruitNameStrong:%p %@", fruit.fruitNameStrong, fruit.fruitNameStrong);// 0x10e27b078 apple
结果是 fruitName, fruitNameCopy, fruitNameStrong 三者的地址都是一样的。按理说copy
动作是深拷贝,fruitNameCopy指向的地址 应该是新分配的内存地址才对。
那为什么打印出来的地址却是同一个地址了?答案是 runtime 在这里做一个性能优化,@“apple” 是一个immutable的值,没有必要做一次深拷贝,直接做一次 retain 就达到目的了。
试试看把NSString
替换成NSArray
对象 Fruit 中添加两个属性 @property (nonatomic, copy) NSArray * placesCopy; @property (nonatomic, strong) NSArray * placesStrong;
先用 mutable 的变量试试
NSMutableArray * places = [NSMutableArray arrayWithArray:@[@"china", @"japan"]];
fruit.placesCopy = places; //placesCopy: 0x7f8f18d0c4f0 (china,japan)
fruit.placesStrong = places; //placesStrong: 0x7f8f18d51a80 (china,japan)
[places addObject:@"korea"];
//placesCopy: 0x7f8f18d0c4f0 (china,japan)
//placesStrong: 0x7f8f18d51a80 (china,japan, korea)
再用 immutable 的变量试试
NSArray * places = @[@"china", @"japan"];
fruit.placesCopy = places; //placesCopy: 0x7fc4e8c6f2a0 (china,japan)
fruit.placesStrong = places; //placesStrong: 0x7fc4e8c6f2a0 (china,japan)
places = @[@"uk", @"us"];
//placesCopy: 0x7fc4e8c6f2a0 (china,japan)
//placesStrong: 0x7fc4e8c6f2a0 (china,japan)
实验结果和 NSString
是一致的。
NSCopying
如果你愿意,你可以试试看其他容器类 NSDictionary
, NSSet
,NSIndexSet
, 会发现结果也保持一致。我们尝试解读这些类的头文件,作进一步的探索。以下是这些类头文件的节选:
@interface NSString : NSObject <NSCopying, NSMutableCopying, NSSecureCoding>
@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>
@interface NSDictionary<__covariant KeyType, __covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>
@interface NSSet<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>
@interface NSIndexSet : NSObject <NSCopying, NSMutableCopying, NSSecureCoding>
@interface NSIndexPath : NSObject <NSCopying, NSSecureCoding>
会发现这些类都实现了NSCoying
协议.在NSCoying
文档中有关于实现该协议的三条准则,其中第三条是:
Implement NSCopying by retaining the original instead of creating a new copy when the class and its contents are immutable.
这条准则很好的解释了上面的试验结果,让这些结果讲得通。
Summary
综上,当我们使用copy
来修饰NSString
等容器类的属性时,如果被拷贝的对象是 Mutable, 则 runtime 会做深拷贝, 如果是 Immutable, 则runtime只是做一次 retain
。
如果在程序中我们自己定义的类也需要实现 NSCopying
协议时,务必也要遵照此规则:若被拷贝的原对象是 Immutable 的,则无需新建一个拷贝,只需要 retian 原对象一次;若原对象是 mutablde 的,则需做一次深拷贝,新建一个对象
如果在程序中,你需要避免一个对象的某个属性被反向更改 (prevent mutating an object’s attributes behind its back),请把该属性标记成 copy