NSString、NSMutableString 可变与不可变的那些事儿

简介

这篇文章的主要包含如下内容:

  • 可变对象和不可变对象
  • NSString 的 copy 和 mutableCopy
  • NSMutableString 的 copy 和 mutableCopy
  • property 中 copy、strong 修饰 NSString
  • property 中 copy、strong 修饰 NSMutableString

很多 iOS 开发的朋友会争论一个问题,我用 copystrong 来修饰 NSString 对象都是一样的效果,在大部分情况下,这二者确实是没有区别,但是在特殊情况下,二者截然不同,所以我们必须搞清楚里面的道道。

我已经尽力简化了这篇文章的内容了,但依然需要你花个15分钟左右的时间,所以当你心情不错又没有其他事情的情况下,就可以来阅读了。

可变对象和不可变对象

Objective-C 中最常用来处理字符串的是 NSStringNSMutableString 这两个类,NSString 被创建赋值后字符串的内容与长度不能再做动态的修改,除非重新给这个字符串赋值。而 NSMutableString 被创建赋值后可以动态的修改字符串的内容。

那么简单来说,可变对象是指,对象的内容是可变的,例如 NSMutableString 对象。不可变的对象则相反,表示其内容不可变,例如 NSString 对象。

可变与不可变是针对对象来说的。在实际开发中,要根据实际的业务场景来选择使用可变还是不可变对象。今天我们只讨论 Objective-CNSStringNSMutableString 这两个类,关于其他集合类的可变与不可变特性,后面专门再来写文章分享。

NSString 的 copy 和 mutableCopy

NSString.h 中,我们可以看到其定义如下:

1
@interface NSString : NSObject <NSCopying, NSMutableCopying, NSSecureCoding>

NSString.h 本身实现了 NSCopying, NSMutableCopying 这两个协议,协议的定义如下内容所示:

1
2
3
4
5
6
7
8
9
10
11
@protocol NSCopying
- (id)copyWithZone:(nullable NSZone *)zone;
@end
@protocol NSMutableCopying
- (id)mutableCopyWithZone:(nullable NSZone *)zone;
@end

也就是说,我们可以针对 NSString 对象进行 copymutableCopy 的操作,妥妥的。

举一个简单的栗子,示例代码如下:

1
2
3
4
5
6
7
8
NSString *name = @"www.";
NSLog(@"name addr: %p, name content: %@", name, name);
NSString *name1 = name;
NSLog(@"name1 addr: %p, name1 content: %@", name1, name1);
NSString *name2 = [name copy];
NSLog(@"name2 addr: %p, name2 content: %@", name2, name2);

输出结果,如下:

1
2
3
name addr: 0x10090ecf8, name content: www.
name1 addr: 0x10090ecf8, name1 content: www.
name2 addr: 0x10090ecf8, name2 content: www.

从输出结果可以看出,三个对象的内容和地址都是一样的,经过 name 对象 copy 后的 name2name 还是指向同一块内存地址。

在断点过程中,发现无论是 name 还是 name1name2 对象,其都是 ConstantString,表明三者都是不可变对象,如下图所示:

从这张图也说明了一个问题,NSString 对象经过 copy 后仍然是不可变对象。

紧接着,我们再来看看 mutableCopy 的使用情况,例子如下:

1
2
3
4
5
6
7
8
9
10
11
NSString *name = @"www.";
NSLog(@"name addr: %p, name content: %@", name, name);
NSString *name1 = name;
NSLog(@"name1 addr: %p, name1 content: %@", name1, name1);
NSString *name2 = [name copy];
NSLog(@"name2 addr: %p, name2 content: %@", name2, name2);
id name3 = [name mutableCopy];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);

对象 name3 是经过 name 对象 mutableCopy 后的,这个时候因为我不确定 name3 到底是可变的还是不可变的,所以采用了 id 来修饰 name3 对象。

可以看一下输出内容:

1
2
3
4
name addr: 0x104a6acf8, name content: www.
name1 addr: 0x104a6acf8, name1 content: www.
name2 addr: 0x104a6acf8, name2 content: www.
name3 addr: 0x1c0052cf0, name3 content: www.

可以看出,name3 的地址变了,再看一下断点的截图:

充分说明了 name3 经过不可变的 name 进行mutableCopy 后变成了可变对象。那么可以将上面的示例代码稍作修改:

1
2
3
4
NSMutableString *name3 = [name mutableCopy];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);
[name3 appendString:@"veryitman.com"];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);

从下面的输出结果也充分说明了 name3 经过不可变的 name 进行 mutableCopy 后变成了可变对象。输出结果如下:

1
name3 addr: 0x1d0058270, name3 content: www.veryitman.com

结论 1:

  • 不可变的 NSString 对象经过 copy 后,还是不可变对象。
  • 不可变的 NSString 对象经过 mutableCopy 后,变成了可变的 NSMutableString 对象。

NSMutableString 的 copy 和 mutableCopy

NSMutableString 继承自 NSString 的,其当然也是实现了 NSCopying, NSMutableCopying 这两个协议的。

1
@interface NSMutableString : NSString

我们还是看例子,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSMutableString *name = [NSMutableString stringWithString:@"www."];
NSLog(@"name addr: %p, name content: %@", name, name);
// 简单赋值
NSMutableString *name1 = name;
NSLog(@"name1 addr: %p, name1 content: %@", name1, name1);
// 使用 copy
id name2 = [name copy];
NSLog(@"name2 addr: %p, name2 content: %@", name2, name2);
// 使用 mutableCopy
id name3 = [name mutableCopy];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);

因为事先我们不知道 NSMutableString 经过 copymutableCopy 之后到底会变成可变还是不可变,上面的例子暂时将 name2name3id 来表示。

断点截图如下:

结合一下输出的日志:

1
2
3
4
name addr: 0x1d044a980, name content: www.
name1 addr: 0x1d044a980, name1 content: www.
name2 addr: 0xa0000002e7777774, name2 content: www.
name3 addr: 0x1d044a5f0, name3 content: www.

可以看出 name2 是一个不可变的 NSString 对象, namename1name3 都是可变的 NSMutableString 对象。

也可以从另外一个角度来验证一下上面的说法,我们修改一下代码:

1
2
3
4
5
6
7
NSMutableString *name2 = [name copy];
NSLog(@"name2 addr: %p, name2 content: %@", name2, name2);
[name2 appendString:@"veryitman.com"];
NSMutableString *name3 = [name mutableCopy];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);
[name3 appendString:@"veryitman.com"];

运行后,可以看到,代码 [name2 appendString:@"veryitman.com"] 这里会引起 Crash,报错内容如下:

1
2
3
4
5
-[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xa0000002e7777774
*** Terminating app due to uncaught exception 'NSInvalidArgumentException'
reason: '-[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xa0000002e7777774'

也充分说明了,name2 是一个不可变的 NSString 对象。

结论 2:

  • 可变的 NSMutableString 对象经过 copy 后,会变成不可变的 NSString 对象。
  • 可变的 NSMutableString 对象经过 mutableCopy 后,仍然是可变的 NSMutableString 对象。

copy、strong 修饰 NSString

创建 Employee 文件,如下:

1
2
3
4
5
@interface Employee : NSObject
@property (nonatomic, copy) NSString *userName;
@end

userName 属性是 copy

使用示例,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Employee *employee = [Employee new];
employee.userName = @"John";
NSLog(@"--before-- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);
// 创建一个可变对象
NSMutableString *newUserName = [[NSMutableString alloc] initWithString:@"new_user_name"];
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);
// 将一个新的对像赋值给 employee.userName,此时 employee.userName 的地址肯定会变化
employee.userName = newUserName;
NSLog(@"---after1--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);
// 试图改变 newUserName 的内容,看 employee.userName 的内容是否改变
[newUserName appendString:@"_hello"];
// newUserName 的内容被改变成了 new_user_name_hello
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);
// employee.userName 的内容未变化
NSLog(@"---after2--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);
// Crash,因为 employee.userName 还是不可变对象
// [(NSMutableString *)(employee.userName) appendString:@"nana"];

在上面的示例中,故意将 NSMutableString 对象 newUserName 赋值给不可变的 NSString 对象 employee.userName,看一下输出结果,如下:

1
2
3
4
5
6
7
8
9
--before-- employee.userName addr: 0x100096cf8, employee.userName content: John
newUserName addr: 0x174070a00, newUserName content: new_user_name
---after1--- employee.userName addr: 0x174023b80, employee.userName content: new_user_name
newUserName addr: 0x174070a00, newUserName content: new_user_name_hello
---after2--- employee.userName addr: 0x174023b80, employee.userName content: new_user_name

按照

1
可变的 `NSMutableString` 对象经过 `copy` 后,会变成不可变的 `NSString` 对象。

这个结论来看,employee.userName 肯定是不可变的对象,即使改变 newUserName 的内容也不会影响 employee.userName 这个对象的内容。

那么,我们将 employee.userName 的属性修饰符 copy 改为 strong,又会是什么样子呢?

我们修改两处代码

Employee.h

1
2
3
4
5
6
7
@interface Employee : NSObject
//@property (nonatomic, copy) NSString *userName;
@property (nonatomic, strong) NSString *userName;
@end

示例代码,只是打开之前会 crash 的部分

1
2
3
// employee.userName 经过 strong 修饰过后, 彻底变成了可变对象
[(NSMutableString *)(employee.userName) appendString:@"_oc"];
NSLog(@"---after3--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

看一下输出日志:

1
2
3
4
5
6
7
8
9
10
11
--before-- employee.userName addr: 0x1000a6cf8, employee.userName content: John
newUserName addr: 0x17426b280, newUserName content: new_user_name
---after1--- employee.userName addr: 0x17426b280, employee.userName content: new_user_name
newUserName addr: 0x17426b280, newUserName content: new_user_name_hello
---after2--- employee.userName addr: 0x17426b280, employee.userName content: new_user_name_hello
---after3--- employee.userName addr: 0x17426b280, employee.userName content: new_user_name_hello_oc

可以看到 employee.userName 最终和 newUserName 的地址、内容完全相同了,彻底变成了可变对象。

另外,如果不是将可变的 NSMutableString 对象赋值给不可变的 NSString 对象,换句话说,NSStringNSString 赋值,那么使用 strongcopy 效果都是一样的。

示例代码(无论 employee.userName 使用 strong 还是 copy,效果都是 employee.userName 不可变的):

1
2
3
4
5
6
7
8
9
10
11
Employee *employee = [Employee new];
employee.userName = @"John";
NSLog(@"--before-- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);
// 创建一个不可变对象
NSString *newUserName = @"new_user_name";
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);
// 将一个新的对像赋值给 employee.userName,此时 employee.userName 的地址肯定会变化
employee.userName = newUserName;
NSLog(@"---after1--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

copy、strong 修饰 NSMutableString

在 property 的修饰语中,只有 copy 修饰语而没有 mutableCopy 的修饰语。

Employee.h

1
2
3
4
5
@interface Employee : NSObject
@property (nonatomic, copy) NSMutableString *userName;
@end

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Employee *employee = [Employee new];
employee.userName = [NSMutableString stringWithString:@"John"];
NSLog(@"--before-- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);
// 创建一个可变对象
NSMutableString *newUserName = [NSMutableString stringWithFormat:@"new_user_name"];
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);
// 将一个新的对像赋值给 employee.userName,此时 employee.userName 的地址肯定会变化
employee.userName = newUserName;
NSLog(@"---after1--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);
// employee.userName 虽然是 NSMutableString 对象,但经过 copy 修饰过后,仍然是不可变对象
// 所以,运行到这里会引起 crash
[employee.userName appendString:@"_oc"];
NSLog(@"---after2--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

可以看出 copy 后的的可变对象还是不可变的。

那么,我们将 employee.userName 的属性修饰符 copy 改为 strong,又会是什么样子呢?

Employee.h

1
2
3
4
5
@interface Employee : NSObject
@property (nonatomic, strong) NSString *userName;
@end

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Employee *employee = [Employee new];
employee.userName = [NSMutableString stringWithString:@"John"];
NSLog(@"--before-- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);
// 创建一个可变对象
NSMutableString *newUserName = [NSMutableString stringWithFormat:@"new_user_name"];
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);
// 将一个新的对像赋值给 employee.userName,此时 employee.userName 的地址肯定会变化
employee.userName = newUserName;
NSLog(@"---after1--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);
// employee.userName 虽然是 NSMutableString 对象,但经过 strong 修饰过后,变成了可变对象
[employee.userName appendString:@"_hello"];
NSLog(@"---after2--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);
[newUserName appendString:@"_oc"];
// newUserName 的内容被改变成了 new_user_name_hello_oc
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);
// employee.userName 的内容发生了变化
NSLog(@"---after2--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

经过 strong 修饰后,可变的 NSMutableString 对象还是可变的对象。

在这个部分的开始,说过在 property 中没有 mutableCopy 的修饰语,那么我们能否达到 mutableCopy 的效果呢?

很显然是可以的,我们可以重写属性的 set 方法,改造一下 Employee 的代码,如下:

Employee.h

1
2
3
4
5
@interface Employee : NSObject
@property (nonatomic, copy) NSMutableString *userName;
@end

Employee.m

1
2
3
4
5
6
7
8
9
10
#import "Employee.h"
@implementation Employee
- (void)setUserName:(NSMutableString *)userName
{
_userName = [userName mutableCopy];
}
@end

这样,就达到了和是 strong 修饰语一样的效果了。

大家,可以使用同样的方法来实践一下 NSArrayNSMutableArry 等集合数据的 copy 以及 mutableCopy 的效果了。

小结

  • 不可变的 NSString 对象经过 copy 后,还是不可变对象。

  • 不可变的 NSString 对象经过 mutableCopy 后,变成了可变的 NSMutableString 对象。

  • 可变的 NSMutableString 对象经过 copy 后,会变成不可变的 NSString 对象。

  • 可变的 NSMutableString 对象经过 mutableCopy 后,仍然是可变的 NSMutableString 对象。

  • 不可变的 NSString 对象在 property 中,尽量使用 copy 来修饰,因为使用 strong 修饰符可变字符串如果给不可变字符串赋值后,会导致你原本预期发生了变化,除非你有特殊的目的才使用 strong 修饰符。

  • 可变的 NSMutableString 对象在 property 中,尽量使用 strong 来修饰,除非你有特殊的目的才使用 copy 修饰符。

  • 虽然在 property 中没有 mutableCopy 修饰符,但是可以通过重写其 set 方法来达到目的。


扫码关注,你我就各多一个朋友~

坚持原创技术分享!