NSString 为什么要用 copy 关键字修饰?

答:

首先根据Apple对NSString的定义:

An immutable string is a text string that is defined when it is created and subsequently cannot be changed. An immutable string is implemented as an array of UTF–16 code units (in other words, a text string). To create and manage an immutable string, use the NSString class. To construct and manage a string that can be changed after it has been created, use NSMutableString. https://developer.apple.com/reference/foundation/nsstring

我们可以知道NSString是不允许改变的字符串。

NSStringNSMutableString的父类,根据里氏替换原则,我们知道父类的指针是可以指向子类指针指向的内存,所以一旦将NSMutableString对象赋给 NSString对象,NSString对象将指向 NSMutableString对象所指向的内存,也就是说,它们指向的将是同一块内存,任何一个引用对象修改了内存里的内容,所有引用对象的值都改变。

用 copy 的话,可以让NSMutableString深拷贝成相同内容的内存,然后NSString对象就可以指向这块独立的内存,不用担心其他的引用对象修改了内存里的内容。

怎么实现的?

猜测

我们知道用@property定义一个成员变量(如: name),编译器会默认加入@synthesize name = _name,从而创建并实现name的 set、get 方法。

可能对有copy关键词修饰的成员变量,编译器会重写 set 方法为 _xxx = [xxx copy]

如果传入的是 NSMutable*对象,NSMutable*对象通过copy方法会深拷贝出相同内容的内存。
如果传入的是 NS*对象,NS*对象通过copy方法会浅拷贝出一个指针从而导致两个对象指向同一块内存,但是NS*对象都是不允许修改的,所以不会有影响。

验证

1
2
3
4
5
6
@interface Foo : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, strong) NSString * strongName;
@end
1
2
3
4
5
6
7
8
9
NSMutableString * string = [[NSMutableString alloc] initWithString:@"123"];
Foo * foo = [Foo new];
foo.name = string;
foo.strongName = string;
[string appendString:@"456"];
NSLog(@"name: %@, strongName: %@", foo.name, foo.strongName);
1
2
log > name: 123, strongName: 123456
// 现在是正常情况下,没有问题。

现在重写name的 set 方法

1
2
3
- (void)setName:(NSString *)name {
_name = name;
}

然后,重复之前的操作

1
2
log > name: 123456, strongName: 123456
// 现在就出错了。

重写一下name的 set 方法,改成_name = [name copy]

1
2
3
- (void)setName:(NSString *)name {
_name = [name copy];
}

重复之前的操作

1
2
log > name: 123, strongName: 123456
// 没有问题。

再用 clang 命令编译成 c++ 文件

1
2
3
4
5
// strongName set方法
static void _I_Foo_setStrongName_(Foo * self, SEL _cmd, NSString *strongName) { (*(NSString **)((char *)self + OBJC_IVAR_$_Foo$_strongName)) = strongName; }
// name set方法
static void _I_Foo_setName_(Foo * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Foo, _name), (id)name, 0, 1); }

可以很明显的发现name的 set 方法和strongName的不一样。

(不知道 objc_setProperty 这个方法的实现在哪,所以到这就不深入了)

在Apple开源的objc4中找到了objc_setProperty方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
...
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
...
}

可见Apple在 set 方法中加入了 [newValue copyWithZone:nil] 也就是 [newValue copy]方法。

谢谢

欢迎勘误,共同进步。