一、集合 & 值语义陷阱(最常见)
1️⃣ for x in array { ... array.append(...) }
for x in array {
array.append(x)
}
为什么危险
- 每次 append 都可能触发 COW
- 实际是 O(n²) + 大量内存拷贝
表面假象
“Swift 的 Array 是值类型,应该很安全吧?”
正确姿势
let original = array
array.reserveCapacity(array.count * 2)
for x in original {
array.append(x)
}
2️⃣ array.filter { ... }.map { ... }
let result = array
.filter { $0.isValid }
.map { transform($0) }
为什么危险
- 产生中间 Array
- 多次遍历
- 内存分配翻倍
隐藏成本
- 每个链式调用都是一次完整遍历
优化
var result = [T]()
result.reserveCapacity(array.count)
for x in array where x.isValid {
result.append(transform(x))
}
(在热路径里非常值)
3️⃣ contains / first(where:) 在循环中
for x in items {
if blacklist.contains(x) { ... }
}
看起来
“一行搞定,超清爽”
实际上
contains是 O(n)- 外层再循环 → O(n²)
解法
let set = Set(blacklist)
for x in items {
if set.contains(x) { ... }
}
二、String / Unicode 暗雷(性能杀手)
4️⃣ String.count
if str.count > 10 { ... }
看起来
“count 不就是长度吗?”
实际上
- Swift 的
String.count是 O(n) - 因为是 grapheme cluster
🔥 在循环里用直接爆炸
替代
if str.utf8.count > 10 { ... }
5️⃣ 用下标访问 String
let c = str[i]
问题
- 每次都要从头遍历
- Unicode 变长编码
后果
- O(n²)
建议
- 转
[UInt8] - 或提前算 index
6️⃣ substring 滥用
let sub = str.prefix(10)
process(String(sub))
危险点
- 新 String
- 拷贝 + ARC
- 在循环中尤其致命
三、ARC & 闭包陷阱(Profiler 里最常见)
7️⃣ 看不见的 retain / release
items.forEach {
self.process($0)
}
问题
- 隐式捕获
self - 每次循环都有 retain/release
更快
for item in items {
process(item)
}
8️⃣ 在循环中创建对象
for _ in 0..<100_000 {
let formatter = DateFormatter()
formatter.string(from: date)
}
为什么致命
- ARC 压力巨大
DateFormatter本身超重
修正
let formatter = DateFormatter()
for _ in 0..<100_000 {
formatter.string(from: date)
}
四、Protocol / 抽象层陷阱
9️⃣ any Protocol
let service: any Service = MyService()
service.run()
看起来
“很现代,很 Swift 5.7”
实际上
- existential container
- 动态派发
- 无法内联
热路径优化
func run<T: Service>(_ s: T) {
s.run()
}
🔟 protocol + associatedtype 的误用
func process(_ p: any Processor) { ... }
后果
- witness table 查找
- 动态类型转换
- 性能非常不稳定
五、并发“伪优化”
1️⃣1️⃣ 滥用 Task {}
for x in items {
Task {
await process(x)
}
}
问题
- 创建 Task 本身就有成本
- 上下文切换比任务还贵
正确心法
- 并发 ≠ 更快
- 少量粗粒度任务
1️⃣2️⃣ async let 用在细粒度循环
for x in items {
async let r = process(x)
results.append(await r)
}
结果
- 顺序 await
- 还多了调度成本
六、Debug 构建下“看起来很慢”
1️⃣3️⃣ 在 Debug 测性能
事实
- Swift Debug 不内联
- ARC 不优化
- 性能和 Release 差一个数量级
⚠️ 永远用 Release + Instruments
七、一句话总结(真·经验)
Swift 的性能灾难
几乎都来自“隐式成本”
而不是你写了多复杂的代码。
最危险的关键词
filter / mapanyString.countforEachTask {}
补充分析:为什么这段代码是正确的?
let original = array
array.reserveCapacity(array.count * 2)
for x in original {
array.append(x)
}
先看“错误但看起来没问题”的版本
for x in array {
array.append(x)
}
Swift 在这里实际做了什么?
for x in array 等价于(概念上):
var iterator = array.makeIterator()
while let x = iterator.next() {
array.append(x)
}
⚠️ 问题来了:
-
iterator持有的是 array 原始 buffer 的视图 -
append会修改 array 本身 -
一旦触发扩容或写入:
- 破坏了 iterator 的一致性
- Swift 为了安全,必须复制 buffer(COW)
结果:
- 每次 append 都可能触发一次 完整数组拷贝
- 复杂度直接从 O(n) → O(n²)
为什么 let original = array 是关键?
let original = array
这一步并没有立即拷贝数据。
Swift 的真实行为(Copy-on-Write)
original和array- 共享同一个 buffer
- 直到某一方发生“写操作”
这时:
array.reserveCapacity(...)
发生了什么?
reserveCapacity是写操作- Swift 发现 buffer 有两个 owner
- 👉 只复制一次
array拿到独立 bufferoriginal继续保持不变
此时关系是:
original ---> old buffer (只读)
array ---> new buffer (可写,容量已扩好)
再看 for 循环为什么安全了
for x in original {
array.append(x)
}
现在:
- 遍历的是 original(只读、不变)
- 修改的是 array(独立 buffer)
- iterator 不会被破坏
- 没有重复 COW
- append 是摊还 O(1)
💥 关键点:
遍历和修改发生在 两个逻辑上不同的数组实例 上
reserveCapacity 为什么也很重要?
如果你写成:
let original = array
for x in original {
array.append(x)
}
仍然是正确的(不会逻辑错误),但:
array每次 append 可能扩容- 扩容会反复分配内存 + 拷贝
reserveCapacity 的作用是:
- 一次性分配足够空间
- 把 append 变成真正的 O(1)
用时间复杂度总结对比
| 写法 | 是否安全 | 时间复杂度 | COW 次数 |
|---|---|---|---|
| 遍历 + 修改同一个 array | ❌ | O(n²) | 多次 |
let original = array | ✅ | O(n) | 1 次 |
+ reserveCapacity | ✅ | O(n) | 1 次(且更快) |
一句话记忆法(非常实用)
Swift 中:
永远不要在遍历一个集合的同时修改它。
要么 copy,要么 split 读写对象。