一、一句话先给结论
Copy-on-Write(COW)= 多个值“共享同一份堆存储”,
只有在“写入且不唯一引用”时才真正拷贝。
目的只有一个:
👉 既保留值语义,又避免不必要的拷贝成本
二、COW 的底层实现原理
1️⃣ 核心结构:值类型 + 引用类型的“混合体”
以 Array 为例(高度简化):
struct Array<Element> {
var buffer: Buffer
}
final class Buffer {
var refCount: Int
var storage: [Element]
}
Array本身是 struct(值类型)- 真正的数据在 堆上的 Buffer(引用类型)
- 多个 Array 可以指向同一个 Buffer
2️⃣ 拷贝发生在什么时候?
var a = [1, 2, 3]
var b = a // ❌ 不拷贝
b.append(4) // ✅ 这里才拷贝
过程是:
a ─┐
├──▶ Buffer [1,2,3] (refCount = 2)
b ─┘
b.append(4)
↓
发现 refCount > 1
↓
复制 Buffer
↓
b ───▶ New Buffer [1,2,3,4]
a ───▶ Old Buffer [1,2,3]
3️⃣ 判断条件:唯一性检查
Swift 在写入前会做:
isKnownUniquelyReferenced(&buffer)
true→ 直接原地修改(不拷贝)false→ 先复制,再修改
这是 COW 的关键 API
三、COW 的完整触发条件(面试重点)
⚠️ 必须同时满足两个条件才会拷贝:
- 发生写操作
- 底层存储不是唯一引用
| 情况 | 是否拷贝 |
|---|---|
| 只读 | ❌ |
| 写 + 唯一引用 | ❌ |
| 写 + 多个引用 | ✅ |
四、哪些类型使用了 COW?
✅ Swift 标准库中
几乎所有“重量级值类型”:
| 类型 | 是否 COW |
|---|---|
Array | ✅ |
Dictionary | ✅ |
Set | ✅ |
String | ✅ |
Substring | ✅ |
Data | ✅ |
ContiguousArray | ✅ |
👉 原则: “体积大 + 值语义”
❌ 不使用 COW 的
| 类型 | 原因 |
|---|---|
Int / Double / Bool | 太小,直接拷贝更快 |
普通 struct | 默认逐字段拷贝 |
enum(无大 payload) | 拷贝成本低 |
五、自定义类型如何实现 COW?
1️⃣ 正确姿势(官方推荐)
struct MyBuffer {
private var storage: Storage
mutating func update(_ v: Int) {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(storage)
}
storage.value = v
}
}
final class Storage {
var value: Int
init(_ other: Storage) {
self.value = other.value
}
}
📌 要点:
- 外层
struct - 内层
final class - 所有
mutating写操作前做唯一性检查
2️⃣ 常见错误 ❌
- 忘了检查唯一性
- 把
Storage暴露出去 - 在多线程中使用
isKnownUniquelyReferenced(非线程安全)
六、COW 与 ARC 的关系
-
COW 依赖 ARC
-
唯一性判断本质是:
- ARC 引用计数 == 1
-
但:
- COW 本身不是 ARC
- COW 是值类型的语义策略
七、几个高频“坑点”
❌ 误区 1:赋值一定会拷贝
var b = a // 不会
❌ 误区 2:值类型一定不共享内存
COW 值类型 共享内存,只是不共享“修改结果”
❌ 误区 3:COW 天然线程安全
❌ 唯一性检查不是原子的
八、一句话面试总结(强烈建议背)
COW 是通过“值类型外壳 + 堆存储 + 写时唯一性检查”实现的,
只在“写 + 非唯一引用”时才拷贝,
广泛用于 Array、Dictionary、String 等重量级值类型。