本文介绍 浅拷贝(Shallow Copy) 与 深拷贝(Deep Copy) 的含义、在 Objective-C / Foundation 中的表现、与内存的关系(引用计数、新对象分配、共享与独立),以及 NSCopying、属性 copy、Swift 值类型与写时拷贝。前置知识见 03-引用计数与MRC详解、04-ARC详解。
一、浅拷贝与深拷贝的定义
1.1 概念
| 类型 | 含义 | 内存上的表现 |
|---|
| 浅拷贝 | 只复制「当前这一层」:得到一个新对象(新指针),但对象内部的元素/子对象仍指向原有的实例。 | 新对象占用新内存(新引用计数);内部元素不复制,多出一份对原元素的引用(引用计数 +1)。 |
| 深拷贝 | 递归复制整棵对象树:当前对象及其内部所有引用到的对象都重新创建一份。 | 整棵对象树都占用新内存,拷贝前后完全独立,无共享引用。 |
- 单层对象(如 NSString、NSData):浅拷贝与深拷贝在「是否共享内容」上的差异,取决于类型是否可变、实现是否共享底层存储(如 copy 后可能共享 buffer,仅引用计数 +1)。
- 集合类(NSArray、NSDictionary 等):浅拷贝 = 新容器 + 元素仍指向原元素;深拷贝 = 新容器 + 对每个元素再递归 copy,需自行实现或使用
initWithArray:copyItems:YES 等 API。
1.2 与内存、引用计数的关系
- 浅拷贝:生成一个新对象(容器或包装类),该对象对内部子对象的引用会使这些子对象的引用计数 +1;子对象本身不复制,内存上共享子对象。
- 深拷贝:生成全新的对象图,每个被拷贝的对象都有新内存、新引用计数;原对象与拷贝无共享,释放一方不影响另一方。
二、Foundation 中的 copy 与 mutableCopy
2.1 常见类型的拷贝语义(概览)
| 类型 | copy | mutableCopy | 说明 |
|---|
| NSString | 不可变副本(可能共享存储,引用计数 +1) | NSMutableString | 不可变 → 不可变 多为浅拷贝;不可变 → 可变 会分配新缓冲 |
| NSMutableString | 不可变 NSString(新内存) | NSMutableString(浅拷贝) | copy 得到不可变,防止外部修改 |
| NSArray | 浅拷贝,新数组、元素仍指向原元素 | NSMutableArray(浅拷贝) | 元素引用计数 +1,元素本身不复制 |
| NSDictionary | 浅拷贝 | NSMutableDictionary(浅拷贝) | 同上 |
| NSData | 浅拷贝(可能共享字节) | NSMutableData | 实现可能共享底层 buffer |
| 自定义类 | 由 copyWithZone: 实现决定 | 由 mutableCopyWithZone: 决定 | 可做浅拷贝或深拷贝 |
- 上述「浅拷贝」指:容器是新对象,元素仍是原对象引用;对容器增删不影响对方,对元素内容的修改可能影响对方(若元素可变)。
2.2 集合的「单层深拷贝」
[[NSArray alloc] initWithArray:array copyItems:YES]:会向每个元素发送 copy,得到新数组 + 一层新元素;若元素本身是集合,其内部不会递归 copy,因此是单层深拷贝,不是递归深拷贝。
- 真正递归深拷贝需自己实现或使用序列化(如
NSKeyedArchiver)再反序列化,注意性能与内存。
三、NSCopying 与 NSMutableCopying
3.1 协议
- NSCopying:实现
- (id)copyWithZone:(NSZone *)zone;调用 [obj copy] 时最终走 copyWithZone:。
- NSMutableCopying:实现
- (id)mutableCopyWithZone:(NSZone *)zone;调用 [obj mutableCopy] 时走 mutableCopyWithZone:。
3.2 拷贝与内存管理
- ARC:copy/mutableCopy 返回的对象由调用方持有(引用计数 +1),遵循 ARC 规则。
- MRC:返回的对象为调用方拥有,需在适当时机 release 或 autorelease;在
copyWithZone: 里返回的对象应为 +1 所有权(alloc 或 copy 出来的)。
3.3 自定义类的浅拷贝与深拷贝示例(概念)
- (id)copyWithZone:(NSZone *)zone {
MyClass *copy = [[MyClass allocWithZone:zone] init];
copy.name = self.name;
copy.child = self.child;
return copy;
}
- (id)copyWithZone:(NSZone *)zone {
MyClass *copy = [[MyClass allocWithZone:zone] init];
copy.name = [self.name copy];
copy.child = [self.child copy];
return copy;
}
- 选择浅拷贝还是深拷贝取决于业务:共享子对象可省内存但需注意多线程/可变性;完全独立则省心但内存与耗时更大。
四、属性的 copy 与内存
4.1 copy 属性
- 声明为
@property (copy) NSString *name 时,setter 会对传入值调用 copy,即持有的是「传入对象的拷贝」的所有权;若传入的是 NSMutableString,拷贝后得到不可变 NSString,避免外部在别处修改导致当前实例被意外改动。
- 对 Block 使用 copy 属性:Block 的 copy 会把栈 Block 拷贝到堆(见 10-Block内存管理),并持有该堆 Block;与「深浅拷贝」中的「拷贝」语义不同,但都涉及「新对象 + 引用计数」。
4.2 深浅拷贝与属性
- 若属性是 集合(如 NSArray),用 copy 只是对集合本身做浅拷贝(新容器、元素仍共享);若希望「外部传入的数组」与内部完全隔离,要么接受浅拷贝(元素共享),要么在 setter 里做一层
initWithArray:copyItems:YES 或自定义深拷贝,并注意内存与性能。
五、内存注意与选型
5.1 浅拷贝
| 优点 | 缺点 |
|---|
| 省内存、速度快 | 与原对象共享子对象;若子对象可变,一边修改会影响另一边;多线程需额外同步 |
5.2 深拷贝
| 优点 | 缺点 |
|---|
| 完全独立,无共享,线程安全更易控制 | 内存与 CPU 开销大,递归深拷贝需防循环引用与栈溢出 |
5.3 何时用哪种
- 浅拷贝:只关心「多一份容器引用」、元素共享可接受(或元素不可变)时;Foundation 的
copy/mutableCopy 默认多为浅拷贝(容器层)。
- 深拷贝:需要「完全独立副本」、避免外部修改或跨线程共享可变状态时;可单层深拷贝(
copyItems:YES)或自定义递归深拷贝。
六、流程图:浅拷贝与深拷贝的内存关系(概念)
flowchart TB
subgraph 浅拷贝
A1[原容器] --> A2[新容器]
A1 --> A3[元素a]
A2 --> A3
end
subgraph 深拷贝
B1[原容器] --> B2[新容器]
B1 --> B3[元素a]
B2 --> B4[元素a 的副本]
end
七、Swift 中的「拷贝」与内存
7.1 值类型与引用类型
- 值类型(struct、enum、基础类型):赋值与传参是拷贝语义(复制一份值);从「不共享同一块堆对象」的角度看,更像「深拷贝」。
- 引用类型(class):赋值与传参是引用,不产生新对象,仅多一个指针;若要独立副本需显式实现拷贝(如实现
NSCopying 或自定义 copy() 方法)。
7.2 写时拷贝(Copy-on-Write)
- Array、Dictionary、Set 等是值类型,但底层存储可能共享 buffer;修改时才复制一份(Copy-on-Write),既保证值语义又减少不必要的内存与拷贝开销。
- 与 OC 的「浅拷贝」不同:Swift 集合的「拷贝」在未修改前可能共享存储,修改时再分配新内存,由标准库保证语义正确。COW 原理、Swift 实现要点(如
isKnownUniquelyReferenced)及与内存的关系见 12-Option与内存优化技术 中的「Copy-on-Write」一节。
八、思维导图:深浅拷贝与内存
mindmap
root((深浅拷贝与内存))
概念
浅拷贝 新对象 共享元素
深拷贝 新对象 递归复制
引用计数
浅拷贝 元素 rc+1
深拷贝 全新对象图
Foundation
copy mutableCopy
集合 copyItems
NSCopying
copyWithZone
自定义浅/深拷贝
属性 copy
setter 调 copy
Block NSString
Swift
值类型 拷贝语义
CopyOnWrite
九、参考文献