一、真实案例:列表 diff 热路径里的 ARC 风暴
背景(真实发生过)
-
iOS 列表 diff(类似 SwiftUI / UICollectionView diff)
-
数据量:几千到上万
-
每次刷新卡顿 40–60ms
-
Time Profiler Top 5:
swift_retainswift_release- closure invoke
二、原始代码(“很 Swift”,但很慢)
final class DiffEngine {
func diff(old: [Item], new: [Item]) -> [Change] {
var changes: [Change] = []
new.enumerated().forEach { index, item in
if let oldItem = old.first(where: { $0.id == item.id }) {
if oldItem != item {
changes.append(.update(index))
}
} else {
changes.append(.insert(index))
}
}
return changes
}
}
看起来的问题点
- forEach + closure
- 捕获
self first(where:)又是 closure- 多层 closure 嵌套
三、Instruments 看到什么?
Time Profiler
swift_retain/swift_release占 ~25%- closure invoke 占 ~15%
Allocations
- closure 对象
- 短生命周期 box
👉 不是算法复杂度问题,是 ARC 在热路径被疯狂调用
四、优化后的代码(结构级改变)
final class DiffEngine {
func diff(old: [Item], new: [Item]) -> [Change] {
var changes: [Change] = []
// 1️⃣ 预构建索引(避免嵌套 closure)
var oldIndex: [Item.ID: Item] = [:]
oldIndex.reserveCapacity(old.count)
for item in old {
oldIndex[item.id] = item
}
// 2️⃣ 纯 for-loop,无 closure
for i in 0..<new.count {
let item = new[i]
if let oldItem = oldIndex[item.id] {
if oldItem != item {
changes.append(.update(i))
}
} else {
changes.append(.insert(i))
}
}
return changes
}
}
五、优化效果(真实数据)
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 总耗时 | ~55ms | ~12ms |
| swift_retain/release | 25% | < 3% |
| closure invoke | 15% | ~0% |
| FPS 掉帧 | 明显 | 消失 |
👉 不是快 10%,是快 4–5 倍
六、这次优化到底“少了哪些 ARC”?
原始代码中的 ARC 来源
forEachclosure- 捕获
self first(where:)closureenumerated()iterator- Item struct 被 box(存在 existential)
优化后
- 无 closure
- 无 self 捕获
- 无 iterator
- 编译器可内联 + 消除 ARC
七、从案例抽象出的「结构性原则」
🧠 原则 1:热路径不用 closure
closure ≠ 坏
closure + 循环 = ARC 放大器
🧠 原则 2:提前“拉平”控制流
- 预计算索引
- 避免嵌套高阶函数
🧠 原则 3:让生命周期“显式化”
let service = self.service
for item in items {
service.process(item)
}
比捕获 self 好得多
🧠 原则 4:用编译器能看懂的结构
- for
- inout
- local let
- final class
八、另一个小但真实的例子(UI state)
原始
state.items.forEach {
self.render($0)
}
优化
let render = self.render
for item in state.items {
render(item)
}
👉 捕获方法引用,避免 retain self
九、如何判断“值不值得优化 ARC”?
我用这个判断表:
| 条件 | 值得 |
|---|---|
| 在 Time Profiler Top 10 | ✅ |
| 在滚动 / 动画路径 | ✅ |
| 每帧 / 高频调用 | ✅ |
| 启动 / 冷路径 | ❌ |
十、最终总结(这是核心)
减少 ARC,不是“减少引用”,而是:
- 减少生命周期抖动
- 减少 heap 边界
- 减少编译器“不确定性”
你不是在“对抗 ARC”,
你是在给编译器创造一个能消除 ARC 的世界。