引言
"为什么改了外面的字符串,我模型里的值也跟着变了?"
"不是说 copy 就是深拷贝吗,怎么元素地址还是一样的?"
如果你也被这些问题困扰过,恭喜你,来对地方了。今天我们从代码验证出发,一次性把 Objective-C 的浅拷贝和深拷贝彻底讲清楚。
一、strong 修饰 NSString 的经典坑
先看一个生产环境里最常见的 bug:
objc
@interface Person : NSObject
@property (nonatomic, strong) NSString *name; // ❌ 错误的写法
@end
// 使用场景
NSMutableString *mutableName = [NSMutableString stringWithString:@"张三"];
self.person.name = mutableName;
[mutableName appendString:@"(已修改)"];
NSLog(@"%@", self.person.name); // "张三(已修改)" --- 内部值被污染了!
为什么?
css
strong 修饰后的内存模型:
mutableName ──→ [NSMutableString "张三"] ←── person.name
(同一个对象,同一块内存)
外部 appendString → 内部也跟着变了
解决方案
objc
@property (nonatomic, copy) NSString *name; // ✅ 标准写法
objectivec
copy 修饰后的内存模型:
mutableName ──→ [NSMutableString "张三"]
person.name ──→ [NSString "张三"] ← 独立不可变副本
外部修改 → 内部不受影响
这就是 copy 和 strong 的本质区别:strong 共享对象,copy 创建独立副本。
二、copy vs mutableCopy:容器层面的行为
很多人以为这两个方法差不多,实际上它们的区别非常明确:
代码验证
objc
// ========== 不可变源 ==========
NSArray *immutable = @[@"A", @"B", @"C"];
NSArray *copyResult = [immutable copy]; // 地址相同!Foundation 优化
NSMutableArray *mutableCopyResult = [immutable mutableCopy]; // 地址不同
// ========== 可变源 ==========
NSMutableArray *mutable = [NSMutableArray arrayWithObjects:@"A", @"B", nil];
NSArray *copyResult2 = [mutable copy]; // 地址不同!创建新不可变容器
NSMutableArray *mutableCopyResult2 = [mutable mutableCopy]; // 地址不同
��为矩阵
| 源对象 | copy |
mutableCopy |
|---|---|---|
不可变 (NSArray) |
返回自身(retain),地址相同 | 新 NSMutableArray,地址不同 |
可变 (NSMutableArray) |
新 NSArray,地址不同 |
新 NSMutableArray,地址不同 |
核心结论:
copy始终返回不可变对象mutableCopy始终返回可变对象- 对不可变源
copy是优化过的,直接 retain,不创建新对象
三、浅拷贝 vs 深拷贝:元素层面才是关键
容器地址不同 ≠ 深拷贝。浅拷贝和深拷贝的核心区别在于元素是否被拷贝。
浅拷贝验证
objc
NSMutableArray *original = [NSMutableArray arrayWithObjects:@"A", @"B", @"C", nil];
NSArray *shallowCopy = [original copy];
NSLog(@"容器: %p vs %p", original, shallowCopy); // 不同 ✅
NSLog(@"元素: %p vs %p", original[0], shallowCopy[0]); // 相同 ← 浅拷贝!
内存模型:
css
original ──→ [容器A] ──→ "A" ←── [容器B] ←── shallowCopy
→ "B" ←─── ↑ 共享
→ "C" ←─── ↑ 共享
结论:copy 和 mutableCopy 对容器元素都是浅拷贝,元素指针共享。
四、真正让元素地址不同的方法
三种常见的"深拷贝"写法:
objc
NSMutableString *a = [NSMutableString stringWithString:@"A"];
NSMutableString *b = [NSMutableString stringWithString:@"B"];
NSMutableString *c = [NSMutableString stringWithString:@"C"];
NSArray *original = @[a, b, c];
// 方法1:归档解档
NSArray *deepCopy = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject:original]];
// 方法2:copyItems:YES
NSArray *deepCopy1 = [[NSArray alloc] initWithArray:original copyItems:YES];
// 方法3:手动递归
NSMutableArray *deepCopy2 = [NSMutableArray array];
for (id obj in original) {
[deepCopy2 addObject:[obj copy]];
}
为什么之前验证时元素地址相同?
因为之前用的是 @"A"(不可变 NSString),它的 copy 就是返回自身:
objc
NSString *str = @"A";
NSString *copied = [str copy];
NSLog(@"%p vs %p", str, copied); // 相同!不可变对象 copy = retain
改成可变元素后,地址果然不同:
objc
NSLog(@"元素: %p VS %p VS %p VS %p",
original[0], deepCopy[0], deepCopy1[0], deepCopy2[0]);
// 元素: 0x600002a00010 VS 0x600002a00030 VS 0x600002a00050 VS 0x600002a00070
// 全部不同!✅
// 验证数据隔离
[a appendString:@"(被篡改)"];
NSLog(@"%@", original[0]); // "A(被篡改)" 不受影响
NSLog(@"%@", deepCopy[0]); // "A" 不受影响 ✅
五、正确的"深拷贝"心智模型
讨论深拷贝时,需要准确定义"深度":
| 层次 | 含义 | 示例 |
|---|---|---|
| 一层深拷贝 | 对第一层可变元素调用 copy,创建不可变副本 |
copyItems:YES |
| 完全深拷贝 | 递归处理所有嵌套层级 | 归档解档 |
核心认知:
"深"不是要求所有元素都必须新建,而是对可变元素 创建独立副本,防止外部修改影响内部。不可变元素本身不会被意外修改,
copy返回自身既安全又高效。
六、三种深拷贝方式的对比
| 方式 | 深度 | 要求 | 性能 | 适用场景 |
|---|---|---|---|---|
copyItems:YES |
一层 | 元素实现 NSCopying |
较快 | 一层嵌套足够 |
| 手动递归 | 可控 | 自己控制深度 | 灵活 | 需要自定义逻辑 |
| 归档解档 | 完全 | 所有元素实现 NSCoding |
最慢 | 需要完全独立副本 |
七、总结
| 概念 | 一句话记忆 |
|---|---|
strong vs copy |
需要保护内部数据的属性用 copy(NSString/NSArray等) |
copy vs mutableCopy |
copy 返回不可变,mutableCopy 返回可变 |
| 浅拷贝 | 新容器 + 旧元素(指针共享) |
| 深拷贝 | 新容器 + 对可变元素创建不可变副本 |