5-28.【性能分析与优化】如何通过代码结构减少 ARC 调用次数?举一个真实优化过的例子。

0 阅读1分钟

一、真实案例:列表 diff 热路径里的 ARC 风暴

背景(真实发生过)

  • iOS 列表 diff(类似 SwiftUI / UICollectionView diff)

  • 数据量:几千到上万

  • 每次刷新卡顿 40–60ms

  • Time Profiler Top 5:

    • swift_retain
    • swift_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/release25%< 3%
closure invoke15%~0%
FPS 掉帧明显消失

👉 不是快 10%,是快 4–5 倍


六、这次优化到底“少了哪些 ARC”?

原始代码中的 ARC 来源

  1. forEach closure
  2. 捕获 self
  3. first(where:) closure
  4. enumerated() iterator
  5. 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 的世界