从 NSString Property:copy or retain 到 NSCopying

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